86 Commits
beta ... stable

Author SHA1 Message Date
gitea-actions[bot]
96d7520fa8 chore(version): bump 01.00.24 → 01.00.25 [skip ci] 2026-04-23 23:34:20 +00:00
gitea-actions[bot]
598605c65c chore: update development SHA-256 for 01.00.24 [skip ci] 2026-04-23 23:30:51 +00:00
gitea-actions[bot]
f6ac970ec0 chore(version): bump 01.00.23 → 01.00.24 [skip ci] 2026-04-23 23:30:46 +00:00
3340c00e26 fix: drawer toggles override btn-outline-secondary with !important
Bootstrap .btn-outline-secondary was overriding the accent color
and border-radius. Use higher specificity + !important.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 23:29:39 +00:00
gitea-actions[bot]
89e255265d chore: update development SHA-256 for 01.00.23 [skip ci] 2026-04-23 23:11:22 +00:00
gitea-actions[bot]
484c223c7e chore(version): bump 01.00.22 → 01.00.23 [skip ci] 2026-04-23 23:11:18 +00:00
1be001993e feat: hero wrapper uses bg variables, default height 75vh
.hero now uses --hero-bg-image, --hero-bg-repeat, --hero-bg-attachment,
--hero-bg-position, --hero-bg-size CSS variables.
Default min-height changed from 100% to 75vh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 23:10:04 +00:00
5b08fe53b5 feat: mod_custom background uses --hero-bg-image CSS variable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 23:10:02 +00:00
gitea-actions[bot]
3b90a375aa chore: update development SHA-256 for 01.00.22 [skip ci] 2026-04-23 23:07:40 +00:00
gitea-actions[bot]
a0dc907f8d chore(version): bump 01.00.21 → 01.00.22 [skip ci] 2026-04-23 23:07:36 +00:00
6f15c3f4f0 refactor: remove CSS variables tab, link to repo docs instead
Replace the large inline CSS variables reference fieldset with a
single note linking to the MokoOnyx repo documentation.
Add docs link to template description.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 23:06:53 +00:00
489ad687af feat: brand buttons use black text by default
All .btn-primary variants now use color: #000 for text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 23:05:28 +00:00
2e69795536 feat: drawer toggle buttons use accent color
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 23:04:10 +00:00
d6f6476454 fix: flat edge on drawer toggle buttons facing the drawer
.drawer-toggle-left gets border-radius 0 on left side.
.drawer-toggle-right gets border-radius 0 on right side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 23:03:28 +00:00
gitea-actions[bot]
592b0ea640 chore: update development SHA-256 for 01.00.21 [skip ci] 2026-04-23 22:55:11 +00:00
gitea-actions[bot]
db584db4af chore(version): bump 01.00.20 → 01.00.21 [skip ci] 2026-04-23 22:55:07 +00:00
12a98762cd fix: hero wrapper full-width, card centering via CSS variables
.hero is the module wrapper — now a full-width flex container.
Card styling (max-width, bg, padding, radius) moves to .hero .card.
All dimensions driven by CSS variables with sensible defaults:
--hero-justify, --hero-align, --hero-min-height,
--hero-card-text-align, --hero-card-max-width, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 22:53:17 +00:00
2f000c330e fix: remove GitHub mirror URLs from updates.xml (404) [skip ci] 2026-04-23 22:43:38 +00:00
gitea-actions[bot]
4977a8c6ff chore: update stable SHA-256 for 01.00.20 [skip ci] 2026-04-23 20:25:57 +00:00
gitea-actions[bot]
c379fab50d chore(version): bump 01.00.19 → 01.00.20 [skip ci] 2026-04-23 20:25:52 +00:00
6d09487aa7 chore: add package-lock.json for npm ci in release workflow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 20:25:36 +00:00
gitea-actions[bot]
a292026966 chore(version): bump 01.00.18 → 01.00.19 [skip ci] 2026-04-23 20:21:35 +00:00
Jonathan Miller
9ce82bdacc fix: remove theme preview tab and theme-test.html
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 15:18:20 -05:00
ce01759455 feat: lock MokoOnyx extension on install and update
Sets locked=1 in #__extensions after install/update to prevent
accidental uninstallation via Extension Manager.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 20:09:29 +00:00
Jonathan Miller
7af3dc0b2d build: minify CSS/JS at release time instead of tracking .min files
All checks were successful
Auto-Assign Issues & PRs / Assign unassigned issues and PRs (pull_request_target) Successful in 1s
- Add scripts/minify.js using clean-css and terser
- Add "Setup Node.js" and "Minify CSS and JS" steps to both
  .gitea and .github release workflows (runs before packaging)
- Add src/media/{css,js}/*.min.{css,js} to .gitignore
- Remove tracked .min files from the repo (vendor/ untouched)

This eliminates noisy diffs on generated files and ensures the
release ZIP always contains freshly minified assets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 08:44:18 -05:00
Jonathan Miller
e98a1d125f docs: document --header-background-color in CSS Variables reference tab
Add the new variable to both en-GB and en-US language strings so it
appears in the template admin panel alongside the existing header
background variables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 08:27:47 -05:00
gitea-actions[bot]
3e9379f300 chore: update development SHA-256 for 01.00.18 [skip ci] 2026-04-23 06:59:18 +00:00
gitea-actions[bot]
e7e71bf77e chore(version): bump 01.00.17 → 01.00.18 [skip ci] 2026-04-23 06:59:16 +00:00
Jonathan Miller
f438398e81 feat(theme): add --header-background-color CSS variable for image-free headers
Fix typo (backgroun-color → background-color) in .container-header that
silently broke the color fallback.  Split background shorthand into
discrete background-color / background-image properties so the color
renders when --header-background-image is set to none.

Add --header-background-color to all four theme files (dark.standard,
light.standard, dark.custom, light.custom).  Regenerate minified CSS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 01:54:05 -05:00
Jonathan Miller
b158e7684c fix: direct API sync for updates.xml (PR blocked by reviews)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 00:00:33 -05:00
Jonathan Miller
765bb9b368 feat: sync updates.xml to main via PR instead of direct API push
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 03:22:22 -05:00
gitea-actions[bot]
659486fac4 chore: update stable SHA-256 for 01.00.17 [skip ci] 2026-04-22 08:15:13 +00:00
gitea-actions[bot]
f2d496b7e5 chore(version): bump 01.00.16 → 01.00.17 [skip ci] 2026-04-22 08:15:10 +00:00
Jonathan Miller
3339dbb620 docs: fix README — dynamic badge, correct requirements, fix links
- Version badge now dynamic from Gitea releases (not hardcoded 03.x)
- PHP requirement corrected to 8.1+ (was 8.0)
- Joomla requirement corrected to 5.x | 6.x (was 4.4.x | 5.x)
- Template overrides section updated (was "No Template Overrides")
- Links corrected from "GitHub" to "Gitea" where appropriate
- Removed stale 03.x changelog entries
- Updated revision history

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 03:03:47 -05:00
gitea-actions[bot]
50331262c9 chore: update development SHA-256 for 01.00.16 [skip ci] 2026-04-22 07:36:50 +00:00
gitea-actions[bot]
f8da2bef6a chore(version): bump 01.00.15 → 01.00.16 [skip ci] 2026-04-22 07:36:48 +00:00
Jonathan Miller
eee3242c4b feat: replace MokoCassiopeia references in content/modules on install/update
Adds replaceCassiopeiaReferences() to script.php postflight so
article content and custom HTML modules are updated during
Joomla admin install/update, not just on frontend page load.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 02:35:27 -05:00
gitea-actions[bot]
157eab4470 chore: update development SHA-256 for 01.00.15 [skip ci] 2026-04-22 07:15:31 +00:00
gitea-actions[bot]
208a072161 chore(version): bump 01.00.14 → 01.00.15 [skip ci] 2026-04-22 07:15:28 +00:00
Jonathan Miller
4423294272 feat: move favicons to template media folder and clean up old locations
- Favicons now output to media/templates/site/mokoonyx/images/favicons/
- On install/update: removes old /images/favicons/ directory
- On install/update: removes stale favicon files from site root

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 02:14:20 -05:00
gitea-actions[bot]
5c014a1484 chore: update development SHA-256 for 01.00.14 [skip ci] 2026-04-22 07:11:37 +00:00
gitea-actions[bot]
d9cc2707bb chore(version): bump 01.00.13 → 01.00.14 [skip ci] 2026-04-22 07:11:35 +00:00
Jonathan Miller
cf0051213f fix: clear favicon stamp on install/update to force regeneration (#1)
Ensures site.webmanifest and all favicon files are regenerated
after every template install or update.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 02:11:10 -05:00
gitea-actions[bot]
17174814ba chore: update development SHA-256 for 01.00.13 [skip ci] 2026-04-22 07:08:45 +00:00
gitea-actions[bot]
ea2ccc482c chore(version): bump 01.00.12 → 01.00.13 [skip ci] 2026-04-22 07:08:42 +00:00
Jonathan Miller
6a40dc558b fix: regenerate favicons when site.webmanifest is missing (#1)
Stamp file check now also verifies manifest exists, preventing
early return that skips generateManifest() on subsequent loads.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 02:08:06 -05:00
gitea-actions[bot]
30bba91a8a chore: update development SHA-256 for 01.00.12 [skip ci] 2026-04-22 02:50:43 +00:00
gitea-actions[bot]
9760cb4a0e chore(version): bump 01.00.11 → 01.00.12 [skip ci] 2026-04-22 02:50:40 +00:00
Jonathan Miller
f6b3f7f0ab fix: use consistent tag names across channels and add component gap
- Stable release tag changed from v01 to 'stable' for consistency
- All updates.xml channels now use channel name as tag
- Add 0.75rem margin below #maincontent component area

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 21:49:33 -05:00
gitea-actions[bot]
7c9850c0a9 chore: update stable SHA-256 for 01.00.11 [skip ci] 2026-04-22 02:08:05 +00:00
gitea-actions[bot]
acff16c0a0 chore(version): bump 01.00.10 → 01.00.11 [skip ci] 2026-04-22 02:08:02 +00:00
Jonathan Miller
0672f63136 chore: sync updates.xml version header and channels to stable 01.00.07
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 21:06:37 -05:00
gitea-actions[bot]
139f1d643e chore: update development SHA-256 for 01.00.10 [skip ci] 2026-04-21 22:48:15 +00:00
gitea-actions[bot]
005efe752c chore(version): bump 01.00.09 → 01.00.10 [skip ci] 2026-04-21 22:48:13 +00:00
Jonathan Miller
fb4f764bc4 feat: add template overrides for Community Builder and DPCalendar modules
Add overrides for all installed third-party modules:
- mod_cblogin (login + logout)
- mod_comprofilermoderator
- mod_comprofileronline
- mod_dpcalendar_counter
- mod_dpcalendar_map
- mod_dpcalendar_mini (with sublayouts)
- mod_dpcalendar_upcoming (with scripts)

Sourced from waas.dev site installation for template consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 17:46:47 -05:00
gitea-actions[bot]
b5cbba7899 chore: update development SHA-256 for 01.00.09 [skip ci] 2026-04-21 22:23:34 +00:00
gitea-actions[bot]
9d0089eac3 chore(version): bump 01.00.08 → 01.00.09 [skip ci] 2026-04-21 22:23:32 +00:00
Jonathan Miller
ee345f1bb6 feat: add custom card module chrome for universal title rendering
Custom card.php layout chrome renders module titles for ALL modules
(core + third-party) when style="card" is used. Fixes missing titles
for Community Builder, DPCalendar, HikaShop, JoomShopping, JS Jobs,
Phoca Gallery, and any other extension modules without individual
template overrides.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 17:22:11 -05:00
Jonathan Miller
edc66d3404 feat: unlock MokoCassiopeia + lock MokoOnyx during migration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 17:06:10 -05:00
Jonathan Miller
51718b2bb8 feat: auto-bump on dev, merge to main via API for stable releases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 16:35:32 -05:00
Jonathan Miller
9a0345defd fix: skip auto-bump on main (branch protection blocks push)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 16:28:31 -05:00
gitea-actions[bot]
bc546cc1e1 chore: update development SHA-256 for 01.00.08 [skip ci] 2026-04-21 21:19:58 +00:00
gitea-actions[bot]
f99715743b chore(version): bump 01.00.07 → 01.00.08 [skip ci] 2026-04-21 21:19:55 +00:00
Jonathan Miller
d903e1e232 feat: add dynamic version badge and migrate content/module references
Add shields.io dynamic version badge (from Gitea releases) to both
templateDetails.xml and sys.ini descriptions. Extend migration script
to replace MokoCassiopeia references in article content and custom
HTML modules. Fix ROADMAP.md repo URLs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 16:18:12 -05:00
gitea-actions[bot]
b6ff24da50 chore: update stable SHA-256 for 01.00.07 [skip ci] 2026-04-21 19:33:47 +00:00
gitea-actions[bot]
748cd855f5 chore(version): bump 01.00.06 → 01.00.07 [skip ci] 2026-04-21 19:33:44 +00:00
gitea-actions[bot]
30660cfee8 chore: update development SHA-256 for 01.00.06 [skip ci] 2026-04-21 19:22:18 +00:00
gitea-actions[bot]
5bd66387e3 chore(version): bump 01.00.05 → 01.00.06 [skip ci] 2026-04-21 19:22:16 +00:00
Jonathan Miller
55d6a3ff7c docs: fill in CLAUDE.md — tokens, auto-bump, multi-channel, migration notes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 14:16:06 -05:00
Jonathan Miller
7c43a9fe64 feat: add Migration tab in template config with re-run instructions
- New "Migration" fieldset in template options
- Instructions for re-running migration (delete .migrated)
- Direct link to Extensions → Manage to uninstall MokoCassiopeia

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 14:09:24 -05:00
Jonathan Miller
b7ab26c999 Update manifest: migration instructions + how to re-run
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 14:06:42 -05:00
gitea-actions[bot]
b965070f3f chore: update development SHA-256 for 01.00.05 [skip ci] 2026-04-21 19:02:01 +00:00
gitea-actions[bot]
084245e9c1 chore(version): bump 01.00.04 → 01.00.05 [skip ci] 2026-04-21 19:01:58 +00:00
Jonathan Miller
602d3f69bc docs: add v02.00.00 roadmap — migration script removal
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 14:00:12 -05:00
gitea-actions[bot]
61aa54df78 chore: update development SHA-256 for 01.00.04 [skip ci] 2026-04-21 18:58:48 +00:00
gitea-actions[bot]
8430ea6804 chore(version): bump 01.00.03 → 01.00.04 [skip ci] 2026-04-21 18:58:45 +00:00
Jonathan Miller
ad1070be03 feat: index.php bootstrap migration from MokoCassiopeia
Joomla 6 doesn't call <scriptfile> for templates. Instead, run
migration on first page load via index.php:
- Detect MokoCassiopeia styles → copy params to MokoOnyx
- Create matching style copies for additional styles
- Set MokoOnyx as default if Cassiopeia was default
- Copy user files (custom themes, user.css, user.js)
- Redirect update server to MokoOnyx
- Creates .migrated marker so it only runs once

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 13:58:18 -05:00
gitea-actions[bot]
8db0a4fa88 chore(version): bump 01.00.02 → 01.00.03 [skip ci] 2026-04-21 18:54:53 +00:00
gitea-actions[bot]
fb8d91c716 chore: update development SHA-256 for 01.00.02 [skip ci] 2026-04-21 18:49:22 +00:00
gitea-actions[bot]
455f5825db chore(version): bump 01.00.01 → 01.00.02 [skip ci] 2026-04-21 18:49:20 +00:00
Jonathan Miller
c9222b4c31 fix: add InstallerScriptInterface for Joomla 6, run migration on install + update
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 13:48:42 -05:00
gitea-actions[bot]
314a4683ae chore: update development SHA-256 for 01.00.01 [skip ci] 2026-04-21 17:42:47 +00:00
gitea-actions[bot]
c1e6a5f42d chore(version): bump 01.00.00 → 01.00.01 [skip ci] 2026-04-21 17:42:44 +00:00
Jonathan Miller
44b823d4f7 feat: add release workflow + migrate MokoCassiopeia styles on install
- .gitea/workflows/release.yml: auto-bump, build ZIP, Gitea release,
  per-channel updates.xml targeting (same as MokoCassiopeia)
- script.php: on install, detect MokoCassiopeia styles, create matching
  MokoOnyx style copies with same params, set default, copy user files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 12:42:22 -05:00
a581da7bdc feat: auto-migrate from MokoCassiopeia on install — copy params, styles, set default
Some checks failed
Repo Health / Access control (push) Successful in 1s
Repo Health / Release configuration (push) Failing after 3s
Repo Health / Scripts governance (push) Successful in 3s
Repo Health / Repository health (push) Failing after 3s
2026-04-21 17:36:13 +00:00
gitea-actions[bot]
67f6b61bf2 chore: update stable SHA-256 for 01.00.00 [skip ci] 2026-04-19 22:30:48 +00:00
52 changed files with 2775 additions and 1203 deletions

View File

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

221
.github/CLAUDE.md vendored
View File

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

View File

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

8
.gitignore vendored
View File

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

View File

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

View File

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

144
package-lock.json generated Normal file
View File

@@ -0,0 +1,144 @@
{
"name": "mokoonyx-build",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mokoonyx-build",
"devDependencies": {
"clean-css": "^5.3.3",
"terser": "^5.39.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/clean-css": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
"integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"source-map": "~0.6.0"
},
"engines": {
"node": ">= 10.0"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"license": "MIT"
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/terser": {
"version": "5.46.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz",
"integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
}
}
}

87
scripts/minify.js Normal file
View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,8 +25,9 @@ $headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'U
if ($params->get('backgroundimage')) {
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $app->getDocument()->getWebAssetManager();
$bgUrl = Uri::root(true) . '/' . HTMLHelper::_('cleanImageURL', $params->get('backgroundimage'))->url;
$wa->addInlineStyle(
'#' . $modId . '{background-image: url("' . Uri::root(true) . '/' . HTMLHelper::_('cleanImageURL', $params->get('backgroundimage'))->url . '");}',
'#' . $modId . '{--hero-bg-image: url("' . $bgUrl . '"); background-image: var(--hero-bg-image);}',
['name' => $modId]
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,4 +28,4 @@ TPL_MOKOONYX_POSITION_TOP_B="Top-b"
TPL_MOKOONYX_POSITION_TOPBAR="Top Bar"
TPL_MOKOONYX_POSITION_DRAWER_LEFT="Drawer-Left"
TPL_MOKOONYX_POSITION_DRAWER_RIGHT="Drawer-Right"
TPL_MOKOONYX_XML_DESCRIPTION="<h3>MokoOnyx Template Description</h3> <p> <strong>MokoOnyx</strong> continues Joomlas tradition of space-themed default templates— building on the legacy of <em>Solarflare</em> (Joomla 1.0), <em>Milkyway</em> (Joomla 1.5), and <em>Protostar</em> (Joomla 3.0). </p> <p> This template is a customized fork of the <strong>Cassiopeia</strong> template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting. </p> <h4>Custom Colour Themes</h4> <p> Starter palette files are included with the template. To create a custom colour scheme, copy <code>templates/mokoonyx/templates/light.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/light.custom.css</code>, or <code>templates/mokoonyx/templates/dark.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/dark.custom.css</code>. Customise the CSS variables to match your brand, then activate your palette in <em>System → Site Templates → MokoOnyx → Theme tab</em> by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the <em>CSS Variables</em> tab in template options. </p> <h4>Custom CSS &amp; JavaScript</h4> <p> For site-specific styles and scripts that should survive template updates, create the following files: </p> <ul> <li><code>media/templates/site/mokoonyx/css/user.css</code> — loaded on every page for custom CSS overrides.</li> <li><code>media/templates/site/mokoonyx/js/user.js</code> — loaded on every page for custom JavaScript.</li> </ul> <p> These files are gitignored and will not be overwritten by template updates. </p> <h4>Code Attribution</h4> <p> This template is based on the original <strong>Cassiopeia</strong> template developed by the <a href=\"https://www.joomla.org\" target=\"_blank\" rel=\"noopener\">Joomla! Project</a> and released under the GNU General Public License. </p> <p> Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards. </p> <p> It includes integration with <a href=\"https://afeld.github.io/bootstrap-toc/\" target=\"_blank\" rel=\"noopener\">Bootstrap TOC</a>, an open-source table of contents generator by A. Feld, licensed under the MIT License. </p> <p> All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable. </p>"
TPL_MOKOONYX_XML_DESCRIPTION="<p><img src=\"https://img.shields.io/gitea/v/release/MokoConsulting/MokoOnyx?gitea_url=https%3A%2F%2Fgit.mokoconsulting.tech&logo=gitea&logoColor=white&label=version\" alt=\"Version\" /> <img src=\"https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&logoColor=white\" alt=\"License\" /> <img src=\"https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white\" alt=\"Joomla\" /> <img src=\"https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white\" alt=\"PHP\" /></p> <h3>MokoOnyx Template Description</h3> <p> <strong>MokoOnyx</strong> continues Joomlas tradition of space-themed default templates— building on the legacy of <em>Solarflare</em> (Joomla 1.0), <em>Milkyway</em> (Joomla 1.5), and <em>Protostar</em> (Joomla 3.0). </p> <p> This template is a customized fork of the <strong>Cassiopeia</strong> template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting. </p> <h4>Custom Colour Themes</h4> <p> Starter palette files are included with the template. To create a custom colour scheme, copy <code>templates/mokoonyx/templates/light.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/light.custom.css</code>, or <code>templates/mokoonyx/templates/dark.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/dark.custom.css</code>. Customise the CSS variables to match your brand, then activate your palette in <em>System → Site Templates → MokoOnyx → Theme tab</em> by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the <em>CSS Variables</em> tab in template options. </p> <h4>Custom CSS &amp; JavaScript</h4> <p> For site-specific styles and scripts that should survive template updates, create the following files: </p> <ul> <li><code>media/templates/site/mokoonyx/css/user.css</code> — loaded on every page for custom CSS overrides.</li> <li><code>media/templates/site/mokoonyx/js/user.js</code> — loaded on every page for custom JavaScript.</li> </ul> <p> These files are gitignored and will not be overwritten by template updates. </p> <h4>Code Attribution</h4> <p> This template is based on the original <strong>Cassiopeia</strong> template developed by the <a href=\"https://www.joomla.org\" target=\"_blank\" rel=\"noopener\">Joomla! Project</a> and released under the GNU General Public License. </p> <p> Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards. </p> <p> It includes integration with <a href=\"https://afeld.github.io/bootstrap-toc/\" target=\"_blank\" rel=\"noopener\">Bootstrap TOC</a>, an open-source table of contents generator by A. Feld, licensed under the MIT License. </p> <p> All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable. </p>"

View File

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

View File

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

View File

@@ -101,26 +101,34 @@ form {
width: 280px;
}
.drawer-toggle-left{
button.drawer-toggle-left,
.drawer-toggle-left.btn {
position: fixed;
top: 50%;
left: 0px;
z-index: 1050;
background-color: var(--nav-bg-color);
color: var(--nav-text-color, gray);
background-color: var(--accent-color, var(--color-primary, #2a69b8)) !important;
color: var(--accent-color-text, #fff) !important;
border-color: var(--accent-color, var(--color-primary, #2a69b8)) !important;
padding-left: .5rem;
padding-right: .5rem;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
.drawer-toggle-right{
button.drawer-toggle-right,
.drawer-toggle-right.btn {
position: fixed;
top: 50%;
right: 0px;
z-index: 1050;
background-color: var(--nav-bg-color);
color: var(--nav-text-color, gray);
background-color: var(--accent-color, var(--color-primary, #2a69b8)) !important;
color: var(--accent-color-text, #fff) !important;
border-color: var(--accent-color, var(--color-primary, #2a69b8)) !important;
padding-left: .5rem;
padding-right: .5rem;
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
hr {
@@ -13623,12 +13631,14 @@ a.navbar-brand {
.btn-primary {
background-color: var(--color-primary, #112855);
border-color: var(--color-primary, #112855);
color: #000;
}
.btn-primary:active,
.btn-primary:focus {
background-color: var(--color-active);
border-color: var(--color-active);
color: #000;
}
.btn-group {
@@ -13925,8 +13935,22 @@ meter {
}
}
/* ── HERO CARD BASE ── */
/* ── HERO WRAPPER (module wrapper, full-width centering container) ── */
.hero {
display: flex;
justify-content: var(--hero-justify, center);
align-items: var(--hero-align, center);
width: 100%;
min-height: var(--hero-min-height, 75vh);
background-image: var(--hero-bg-image, none);
background-repeat: var(--hero-bg-repeat, no-repeat);
background-attachment: var(--hero-bg-attachment, fixed);
background-position: var(--hero-bg-position, center);
background-size: var(--hero-bg-size, cover);
}
/* ── HERO CARD BASE (the .card inside the hero wrapper) ── */
.hero .card {
max-width: var(--hero-card-max-width, 600px);
padding: var(--hero-card-padding-y, 3rem) var(--hero-card-padding-x, 2rem);
background-color: var(--hero-card-bg, var(--hero-primary-bg-color, #0d1e3a));
@@ -13935,17 +13959,21 @@ meter {
background-position: center;
color: var(--hero-card-color, var(--hero-primary-color, #f1f5f9));
border-radius: var(--hero-card-border-radius, .5rem);
text-align: var(--hero-card-text-align, center);
margin-left: auto;
margin-right: auto;
border: none;
}
/* ── PRIMARY VARIANT (uses default card vars) ── */
.hero#primary {
/* ── PRIMARY VARIANT ── */
.hero#primary .card {
background-color: var(--hero-card-bg, #0d1e3a);
background-image: var(--hero-card-overlay, none);
color: var(--hero-card-color, #f1f5f9);
}
/* ── SECONDARY / ALTERNATIVE VARIANT ── */
.hero#secondary {
.hero#secondary .card {
max-width: var(--hero-alt-card-max-width, 600px);
padding: var(--hero-alt-card-padding-y, 3rem) var(--hero-alt-card-padding-x, 2rem);
background-color: var(--hero-alt-card-bg, #080f1e);
@@ -13967,14 +13995,14 @@ meter {
background-image: none;
}
.hero {
max-width: 100%;
width: 100%;
min-height: 100vh;
min-height: 100dvh;
border-radius: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.hero .card {
max-width: 100%;
width: 100%;
border-radius: 0;
}
}
@@ -14193,10 +14221,11 @@ fieldset>* {
.container-header {
z-index: 100;
background: var(--header-background-image, url('../../../../../../media/templates/site/mokoonyx/images/bg.svg'));
background-color: var(--header-background-color, #adadad);
background-image: var(--header-background-image, url('../../../../../../media/templates/site/mokoonyx/images/bg.svg'));
background-size: var(--header-background-size, auto);
box-shadow: 0 5px 5px hsla(0, 0%, 0%, 0.03) inset;
background-repeat: var(--header-background-repeat, repeat);
box-shadow: 0 5px 5px hsla(0, 0%, 0%, 0.03) inset;
}
/* Sticky header: override z-index to stay above all content */
@@ -14994,6 +15023,10 @@ iframe {
margin-bottom: 0;
}
#maincontent {
margin-bottom: 0.75rem;
}
.container-component,
.sidebar-left,
.sidebar-right {
@@ -16955,11 +16988,14 @@ body:not(.has-sidebar-right) .site-grid .container-component {
}
.btn-primary {
--bs-btn-color: #000;
--bs-btn-bg: #333;
--bs-btn-border-color: #333;
--bs-btn-hover-color: #000;
--bs-btn-hover-bg: #555;
--bs-btn-hover-border-color: #555;
--bs-btn-focus-shadow-rgb: 49, 132, 253;
--bs-btn-active-color: #000;
--bs-btn-active-bg: #555;
--bs-btn-active-border-color: #555;
--bs-btn-disabled-bg: #A0A0A0;
@@ -18945,7 +18981,7 @@ nav[data-toggle=toc] .nav-link.active+ul{
/* ===== BOOTSTRAP & JOOMLA BUTTONS ===== */
.btn-primary {
--btn-color: white;
--btn-color: #000;
--btn-bg: var(--color-primary, #112855);
--btn-border-color: var(--color-primary, #112855);
--btn-hover-bg: color-mix(in srgb, var(--color-primary, #112855) 85%, black);

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@@ -39,13 +39,13 @@
</server>
</updateservers>
<name>MokoOnyx</name>
<version>01.00.00</version>
<version>01.00.25</version>
<scriptfile>script.php</scriptfile>
<creationDate>2026-04-15</creationDate>
<author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright>
<description><![CDATA[<p><img src="https://img.shields.io/badge/version-01.00.00-blue.svg?logo=v&amp;logoColor=white" alt="Version 01.00.00" /> <img src="https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&amp;logoColor=white" alt="License" /> <img src="https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&amp;logoColor=white" alt="Joomla" /> <img src="https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&amp;logoColor=white" alt="PHP" /></p> <h3>MokoOnyx Template Description</h3> <p> <strong>MokoOnyx</strong> continues Joomla's tradition of space-themed default templates— building on the legacy of <em>Solarflare</em> (Joomla 1.0), <em>Milkyway</em> (Joomla 1.5), and <em>Protostar</em> (Joomla 3.0). </p> <p> This template is a customized fork of the <strong>Cassiopeia</strong> template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting. </p> <h4>Custom Colour Themes</h4> <p> Starter palette files are included with the template. To create a custom colour scheme, copy <code>templates/mokoonyx/templates/light.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/light.custom.css</code>, or <code>templates/mokoonyx/templates/dark.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/dark.custom.css</code>. Customise the CSS variables to match your brand, then activate your palette in <em>System → Site Templates → MokoOnyx → Theme tab</em> by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the <em>CSS Variables</em> tab in template options. </p> <h4>Custom CSS &amp; JavaScript</h4> <p> For site-specific styles and scripts that should survive template updates, create the following files: </p> <ul> <li><code>media/templates/site/mokoonyx/css/user.css</code> — loaded on every page for custom CSS overrides.</li> <li><code>media/templates/site/mokoonyx/js/user.js</code> — loaded on every page for custom JavaScript.</li> </ul> <p> These files are gitignored and will not be overwritten by template updates. </p> <h4>Code Attribution</h4> <p> This template is based on the original <strong>Cassiopeia</strong> template developed by the <a href="https://www.joomla.org" target="_blank" rel="noopener">Joomla! Project</a> and released under the GNU General Public License. </p> <p> Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards. </p> <p> It includes integration with <a href="https://afeld.github.io/bootstrap-toc/" target="_blank" rel="noopener">Bootstrap TOC</a>, an open-source table of contents generator by A. Feld, licensed under the MIT License. </p> <p> All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable. </p>]]></description>
<description><![CDATA[<div style="padding:1rem;border:2px solid #059669;border-radius:8px;background:#ecfdf5;margin-bottom:1rem;"> <h3 style="color:#065f46;margin:0 0 .5rem;">Migrating from MokoCassiopeia?</h3> <p style="margin:0 0 .75rem;">MokoOnyx automatically imports your MokoCassiopeia settings on first use. To trigger the migration:</p> <ol style="margin:0 0 .75rem 1.5rem;"> <li>Install MokoOnyx via <strong>System → Install → Extensions</strong></li> <li>Go to <strong>System → Site Templates</strong> and set <strong>MokoOnyx</strong> as your default template</li> <li><strong>Visit any page on your site</strong> — the migration runs automatically on first page load</li> <li>Check <strong>administrator/logs/mokoonyx_migrate.log.php</strong> to confirm migration completed</li> <li>Uninstall MokoCassiopeia from <strong>Extensions → Manage</strong></li> </ol> <p style="margin:0;font-size:.9em;color:#065f46;"> <strong>What gets migrated:</strong> template style params, custom colour palettes (light.custom.css, dark.custom.css), user.css, user.js, and update server URL. To re-run the migration, delete <code>templates/mokoonyx/.migrated</code> and reload any page. </p></div> <p><img src="https://img.shields.io/gitea/v/release/MokoConsulting/MokoOnyx?gitea_url=https%3A%2F%2Fgit.mokoconsulting.tech&amp;logo=gitea&amp;logoColor=white&amp;label=version" alt="Version" /> <img src="https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&amp;logoColor=white" alt="License" /> <img src="https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&amp;logoColor=white" alt="Joomla" /> <img src="https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&amp;logoColor=white" alt="PHP" /></p> <h3>MokoOnyx</h3> <p> <strong>MokoOnyx</strong> (formerly MokoCassiopeia) is a modern, lightweight enhancement layer built on Joomla's Cassiopeia template. It adds Font Awesome 7, Bootstrap 5 helpers, automatic Table of Contents, advanced Dark Mode theming, and optional Google Tag Manager / GA4 integration — all with minimal core overrides for maximum upgrade compatibility. </p> <h4>Custom Colour Themes</h4> <p> Copy <code>templates/mokoonyx/templates/light.custom.css</code> to <code>media/templates/site/mokoonyx/css/theme/light.custom.css</code> (or dark equivalent), customise the CSS variables, then select "Custom" in the Theme tab. </p> <h4>Custom CSS &amp; JavaScript</h4> <ul> <li><code>media/templates/site/mokoonyx/css/user.css</code> — custom CSS overrides</li> <li><code>media/templates/site/mokoonyx/js/user.js</code> — custom JavaScript</li> </ul> <p>These files survive template updates.</p> <h4>CSS Variables Reference</h4> <p>For a complete list of CSS variables, see the <a href="https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx" target="_blank">MokoOnyx repository documentation</a>.</p>]]></description>
<inheritable>1</inheritable>
<files>
<filename>component.php</filename>
@@ -108,6 +108,10 @@
<fields name="params">
<!-- Advanced tab (non-theme/system options only) -->
<fieldset name="migration" label="TPL_MOKOONYX_MIGRATION_FIELDSET_LABEL">
<field name="migration_note" type="note" label="TPL_MOKOONYX_MIGRATION_NOTE_LABEL" description="TPL_MOKOONYX_MIGRATION_NOTE_DESC" />
<field name="migration_run" type="note" label="TPL_MOKOONYX_MIGRATION_RUN_LABEL" description="TPL_MOKOONYX_MIGRATION_RUN_DESC" />
</fieldset>
<fieldset name="advanced">
<field name="developmentmode" type="radio" label="TPL_MOKOONYX_DEVELOPMENTMODE_LABEL" description="TPL_MOKOONYX_DEVELOPMENTMODE_DESC" default="0" layout="joomla.form.field.radio.switcher" filter="boolean">
<option value="0">JNO</option>
@@ -323,57 +327,11 @@
</field>
</fieldset>
<!-- CSS Variables reference tab -->
<!-- CSS Variables: see repo documentation -->
<fieldset name="css_variables" label="TPL_MOKOONYX_CSS_VARS_FIELDSET_LABEL">
<field name="css_vars_intro" type="note" description="TPL_MOKOONYX_CSS_VARS_INTRO" />
<field name="css_vars_brand" type="note" label="TPL_MOKOONYX_CSS_VARS_BRAND_LABEL" description="TPL_MOKOONYX_CSS_VARS_BRAND_DESC" class="alert alert-light" />
<field name="css_vars_links" type="note" label="TPL_MOKOONYX_CSS_VARS_LINKS_LABEL" description="TPL_MOKOONYX_CSS_VARS_LINKS_DESC" class="alert alert-light" />
<field name="css_vars_typography" type="note" label="TPL_MOKOONYX_CSS_VARS_TYPO_LABEL" description="TPL_MOKOONYX_CSS_VARS_TYPO_DESC" class="alert alert-light" />
<field name="css_vars_nav" type="note" label="TPL_MOKOONYX_CSS_VARS_NAV_LABEL" description="TPL_MOKOONYX_CSS_VARS_NAV_DESC" class="alert alert-light" />
<field name="css_vars_layout" type="note" label="TPL_MOKOONYX_CSS_VARS_LAYOUT_LABEL" description="TPL_MOKOONYX_CSS_VARS_LAYOUT_DESC" class="alert alert-light" />
<field name="css_vars_breakpoints" type="note" label="TPL_MOKOONYX_CSS_VARS_BP_LABEL" description="TPL_MOKOONYX_CSS_VARS_BP_DESC" class="alert alert-light" />
<field name="css_vars_bootstrap" type="note" label="TPL_MOKOONYX_CSS_VARS_BS_LABEL" description="TPL_MOKOONYX_CSS_VARS_BS_DESC" class="alert alert-light" />
<field name="css_vars_bootstrap_states" type="note" label="TPL_MOKOONYX_CSS_VARS_BS_STATES_LABEL" description="TPL_MOKOONYX_CSS_VARS_BS_STATES_DESC" class="alert alert-light" />
<field name="css_vars_alert_list" type="note" label="TPL_MOKOONYX_CSS_VARS_ALERT_LIST_LABEL" description="TPL_MOKOONYX_CSS_VARS_ALERT_LIST_DESC" class="alert alert-light" />
<field name="css_vars_colors" type="note" label="TPL_MOKOONYX_CSS_VARS_COLORS_LABEL" description="TPL_MOKOONYX_CSS_VARS_COLORS_DESC" class="alert alert-light" />
<field name="css_vars_hero" type="note" label="TPL_MOKOONYX_CSS_VARS_HERO_LABEL" description="TPL_MOKOONYX_CSS_VARS_HERO_DESC" class="alert alert-light" />
<field name="css_vars_hero_variants" type="note" label="TPL_MOKOONYX_CSS_VARS_HERO_VARIANTS_LABEL" description="TPL_MOKOONYX_CSS_VARS_HERO_VARIANTS_DESC" class="alert alert-info" />
<field name="css_vars_block_colors" type="note" label="TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_LABEL" description="TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_DESC" class="alert alert-info" />
<field name="css_vars_header" type="note" label="TPL_MOKOONYX_CSS_VARS_HEADER_LABEL" description="TPL_MOKOONYX_CSS_VARS_HEADER_DESC" class="alert alert-light" />
<field name="css_vars_containers" type="note" label="TPL_MOKOONYX_CSS_VARS_CONTAINERS_LABEL" description="TPL_MOKOONYX_CSS_VARS_CONTAINERS_DESC" class="alert alert-light" />
<field name="css_vars_borders" type="note" label="TPL_MOKOONYX_CSS_VARS_BORDERS_LABEL" description="TPL_MOKOONYX_CSS_VARS_BORDERS_DESC" class="alert alert-light" />
<field name="css_vars_shadows" type="note" label="TPL_MOKOONYX_CSS_VARS_SHADOWS_LABEL" description="TPL_MOKOONYX_CSS_VARS_SHADOWS_DESC" class="alert alert-light" />
<field name="css_vars_forms" type="note" label="TPL_MOKOONYX_CSS_VARS_FORMS_LABEL" description="TPL_MOKOONYX_CSS_VARS_FORMS_DESC" class="alert alert-light" />
<field name="css_vars_buttons" type="note" label="TPL_MOKOONYX_CSS_VARS_BUTTONS_LABEL" description="TPL_MOKOONYX_CSS_VARS_BUTTONS_DESC" class="alert alert-light" />
<field name="css_vars_cards" type="note" label="TPL_MOKOONYX_CSS_VARS_CARDS_LABEL" description="TPL_MOKOONYX_CSS_VARS_CARDS_DESC" class="alert alert-light" />
<field name="css_vars_accordion" type="note" label="TPL_MOKOONYX_CSS_VARS_ACCORDION_LABEL" description="TPL_MOKOONYX_CSS_VARS_ACCORDION_DESC" class="alert alert-light" />
<field name="css_vars_alert_base" type="note" label="TPL_MOKOONYX_CSS_VARS_ALERT_BASE_LABEL" description="TPL_MOKOONYX_CSS_VARS_ALERT_BASE_DESC" class="alert alert-light" />
<field name="css_vars_badge" type="note" label="TPL_MOKOONYX_CSS_VARS_BADGE_LABEL" description="TPL_MOKOONYX_CSS_VARS_BADGE_DESC" class="alert alert-light" />
<field name="css_vars_breadcrumb" type="note" label="TPL_MOKOONYX_CSS_VARS_BREADCRUMB_LABEL" description="TPL_MOKOONYX_CSS_VARS_BREADCRUMB_DESC" class="alert alert-light" />
<field name="css_vars_dropdown_menu" type="note" label="TPL_MOKOONYX_CSS_VARS_DROPDOWN_MENU_LABEL" description="TPL_MOKOONYX_CSS_VARS_DROPDOWN_MENU_DESC" class="alert alert-light" />
<field name="css_vars_list_group" type="note" label="TPL_MOKOONYX_CSS_VARS_LIST_GROUP_LABEL" description="TPL_MOKOONYX_CSS_VARS_LIST_GROUP_DESC" class="alert alert-light" />
<field name="css_vars_modal" type="note" label="TPL_MOKOONYX_CSS_VARS_MODAL_LABEL" description="TPL_MOKOONYX_CSS_VARS_MODAL_DESC" class="alert alert-light" />
<field name="css_vars_backdrop" type="note" label="TPL_MOKOONYX_CSS_VARS_BACKDROP_LABEL" description="TPL_MOKOONYX_CSS_VARS_BACKDROP_DESC" class="alert alert-light" />
<field name="css_vars_nav_tabs" type="note" label="TPL_MOKOONYX_CSS_VARS_NAV_TABS_LABEL" description="TPL_MOKOONYX_CSS_VARS_NAV_TABS_DESC" class="alert alert-light" />
<field name="css_vars_nav_pills" type="note" label="TPL_MOKOONYX_CSS_VARS_NAV_PILLS_LABEL" description="TPL_MOKOONYX_CSS_VARS_NAV_PILLS_DESC" class="alert alert-light" />
<field name="css_vars_pagination" type="note" label="TPL_MOKOONYX_CSS_VARS_PAGINATION_LABEL" description="TPL_MOKOONYX_CSS_VARS_PAGINATION_DESC" class="alert alert-light" />
<field name="css_vars_popover" type="note" label="TPL_MOKOONYX_CSS_VARS_POPOVER_LABEL" description="TPL_MOKOONYX_CSS_VARS_POPOVER_DESC" class="alert alert-light" />
<field name="css_vars_progress" type="note" label="TPL_MOKOONYX_CSS_VARS_PROGRESS_LABEL" description="TPL_MOKOONYX_CSS_VARS_PROGRESS_DESC" class="alert alert-light" />
<field name="css_vars_spinner" type="note" label="TPL_MOKOONYX_CSS_VARS_SPINNER_LABEL" description="TPL_MOKOONYX_CSS_VARS_SPINNER_DESC" class="alert alert-light" />
<field name="css_vars_table" type="note" label="TPL_MOKOONYX_CSS_VARS_TABLE_LABEL" description="TPL_MOKOONYX_CSS_VARS_TABLE_DESC" class="alert alert-light" />
<field name="css_vars_toast" type="note" label="TPL_MOKOONYX_CSS_VARS_TOAST_LABEL" description="TPL_MOKOONYX_CSS_VARS_TOAST_DESC" class="alert alert-light" />
<field name="css_vars_tooltip" type="note" label="TPL_MOKOONYX_CSS_VARS_TOOLTIP_LABEL" description="TPL_MOKOONYX_CSS_VARS_TOOLTIP_DESC" class="alert alert-light" />
<field name="css_vars_components" type="note" label="TPL_MOKOONYX_CSS_VARS_COMPONENTS_LABEL" description="TPL_MOKOONYX_CSS_VARS_COMPONENTS_DESC" class="alert alert-light" />
<field name="css_vars_offcanvas" type="note" label="TPL_MOKOONYX_CSS_VARS_OFFCANVAS_LABEL" description="TPL_MOKOONYX_CSS_VARS_OFFCANVAS_DESC" class="alert alert-light" />
<field name="css_vars_virtuemart" type="note" label="TPL_MOKOONYX_CSS_VARS_VM_LABEL" description="TPL_MOKOONYX_CSS_VARS_VM_DESC" class="alert alert-light" />
<field name="css_vars_gable" type="note" label="TPL_MOKOONYX_CSS_VARS_GABLE_LABEL" description="TPL_MOKOONYX_CSS_VARS_GABLE_DESC" class="alert alert-light" />
<field name="css_vars_footer" type="note" label="TPL_MOKOONYX_CSS_VARS_FOOTER_LABEL" description="TPL_MOKOONYX_CSS_VARS_FOOTER_DESC" class="alert alert-light" />
</fieldset>
<!-- Theme Preview tab — embedded test sheet -->
<fieldset name="theme_preview" label="TPL_MOKOONYX_THEME_PREVIEW_FIELDSET_LABEL">
<field name="theme_preview_intro" type="note" description="TPL_MOKOONYX_THEME_PREVIEW_INTRO" />
<field name="theme_preview_frame" type="note" description="TPL_MOKOONYX_THEME_PREVIEW_FRAME" />
<field name="css_vars_docs_link" type="note"
description="For a full list of CSS variables and theming options, see the &lt;a href='https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx' target='_blank'&gt;MokoOnyx documentation&lt;/a&gt;."
class="alert alert-info" />
</fieldset>
</fields>
</config>

View File

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

View File

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

View File

@@ -1,836 +0,0 @@
<!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
-->
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MokoOnyx — Theme Test Sheet</title>
<!-- Load the template CSS -->
<link rel="stylesheet" href="../media/css/template.css">
<!-- Load the light custom palette (swap to dark.custom.css for dark mode testing) -->
<link rel="stylesheet" href="light.custom.css">
<style>
/* ── Test Page Layout ── */
body { font-family: var(--body-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif); color: var(--body-color, #22262a); background: var(--body-bg, #fff); margin: 0; padding: 0; }
.test-container { max-width: 1200px; margin: 0 auto; padding: 1rem 1.5rem; }
h1, h2, h3, h4, h5, h6 { color: var(--heading-color, inherit); }
h1 { font-size: 2.25rem; margin-bottom: .25rem; }
h2 { font-size: 1.75rem; margin-top: 2.5rem; border-bottom: 2px solid var(--border-color, #dfe3e7); padding-bottom: .5rem; }
h3 { font-size: 1.25rem; margin-top: 1.5rem; }
p.lead { font-size: 1.15rem; color: var(--muted-color, #6d757e); }
hr { border: 0; border-top: 1px solid var(--border-color, #dfe3e7); margin: 2rem 0; }
a { color: var(--link-color, #224faa); text-decoration: var(--link-decoration, underline); }
a:hover { color: var(--link-hover-color, #424077); }
code { color: var(--code-color, #e93f8e); background: var(--secondary-bg, #eaedf0); padding: .15em .4em; border-radius: .2rem; font-size: .875em; }
pre { background: var(--secondary-bg, #eaedf0); color: var(--body-color); padding: 1rem; border-radius: var(--border-radius, .25rem); overflow-x: auto; font-size: .875rem; }
/* ── Swatch Grid ── */
.swatch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: .75rem; margin: 1rem 0; }
.swatch { border-radius: var(--border-radius, .25rem); border: 1px solid var(--border-color, #dfe3e7); overflow: hidden; }
.swatch-color { height: 60px; }
.swatch-label { padding: .4rem .6rem; font-size: .75rem; background: var(--body-bg, #fff); }
.swatch-label code { font-size: .7rem; }
/* ── Variable Table ── */
.var-table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: .875rem; }
.var-table th, .var-table td { padding: .5rem .75rem; border: 1px solid var(--border-color, #dfe3e7); text-align: left; }
.var-table th { background: var(--secondary-bg, #eaedf0); font-weight: 600; }
.var-table tr:nth-child(even) td { background: var(--tertiary-bg, #f9fafb); }
/* ── Flex row helper ── */
.row { display: flex; flex-wrap: wrap; gap: 1rem; }
.col { flex: 1; min-width: 200px; }
/* ── Theme Toggle ── */
.theme-toggle { position: fixed; top: 1rem; right: 1.5rem; z-index: 1000; }
.theme-toggle button { padding: .5rem 1rem; border: 1px solid var(--border-color); border-radius: var(--border-radius); background: var(--body-bg); color: var(--body-color); cursor: pointer; font-size: .875rem; }
/* ── Block Color Demo ── */
.block-demo { display: flex; gap: .75rem; flex-wrap: wrap; margin: 1rem 0; }
.block-demo .card { flex: 1; min-width: 180px; padding: 1.25rem; border-radius: var(--border-radius, .25rem); border: 1px solid var(--border-color, #dfe3e7); }
</style>
</head>
<body>
<div class="theme-toggle">
<button onclick="toggleTheme()">Toggle Light / Dark</button>
</div>
<div class="test-container">
<!-- ══════════════════════════════════════════════
HEADER
══════════════════════════════════════════════ -->
<h1>MokoOnyx Theme Test Sheet</h1>
<p class="lead">Visual reference for CSS variables, Bootstrap components, hero variants, and block color system. Toggle light/dark mode with the button in the top-right corner.</p>
<hr>
<!-- ══════════════════════════════════════════════
1. BRAND COLORS
══════════════════════════════════════════════ -->
<h2>1. Brand &amp; Theme Colors</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--color-primary)"></div>
<div class="swatch-label"><code>--color-primary</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--accent-color-primary)"></div>
<div class="swatch-label"><code>--accent-color-primary</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--accent-color-secondary)"></div>
<div class="swatch-label"><code>--accent-color-secondary</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--body-bg)"></div>
<div class="swatch-label"><code>--body-bg</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--body-color)"></div>
<div class="swatch-label"><code>--body-color</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--secondary-bg)"></div>
<div class="swatch-label"><code>--secondary-bg</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--tertiary-bg)"></div>
<div class="swatch-label"><code>--tertiary-bg</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--border-color)"></div>
<div class="swatch-label"><code>--border-color</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
2. BOOTSTRAP PALETTE
══════════════════════════════════════════════ -->
<h2>2. Bootstrap Color Palette</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--primary)"></div>
<div class="swatch-label"><code>--primary</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--secondary)"></div>
<div class="swatch-label"><code>--secondary</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--success)"></div>
<div class="swatch-label"><code>--success</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--info)"></div>
<div class="swatch-label"><code>--info</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--warning)"></div>
<div class="swatch-label"><code>--warning</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--danger)"></div>
<div class="swatch-label"><code>--danger</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--light)"></div>
<div class="swatch-label"><code>--light</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--dark)"></div>
<div class="swatch-label"><code>--dark</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
3. GRAY SCALE
══════════════════════════════════════════════ -->
<h2>3. Gray Scale</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-100)"></div>
<div class="swatch-label"><code>--gray-100</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-200)"></div>
<div class="swatch-label"><code>--gray-200</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-300)"></div>
<div class="swatch-label"><code>--gray-300</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-400)"></div>
<div class="swatch-label"><code>--gray-400</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-500)"></div>
<div class="swatch-label"><code>--gray-500</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-600)"></div>
<div class="swatch-label"><code>--gray-600</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-700)"></div>
<div class="swatch-label"><code>--gray-700</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-800)"></div>
<div class="swatch-label"><code>--gray-800</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gray-900)"></div>
<div class="swatch-label"><code>--gray-900</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
4. STANDARD COLORS
══════════════════════════════════════════════ -->
<h2>4. Standard Colors</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--blue)"></div>
<div class="swatch-label"><code>--blue</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--indigo)"></div>
<div class="swatch-label"><code>--indigo</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--purple)"></div>
<div class="swatch-label"><code>--purple</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--pink)"></div>
<div class="swatch-label"><code>--pink</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--red)"></div>
<div class="swatch-label"><code>--red</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--orange)"></div>
<div class="swatch-label"><code>--orange</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--yellow)"></div>
<div class="swatch-label"><code>--yellow</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--green)"></div>
<div class="swatch-label"><code>--green</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--teal)"></div>
<div class="swatch-label"><code>--teal</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--cyan)"></div>
<div class="swatch-label"><code>--cyan</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
5. TYPOGRAPHY
══════════════════════════════════════════════ -->
<h2>5. Typography</h2>
<div>
<h1>Heading 1 <small style="color: var(--muted-color); font-size: .5em;">h1</small></h1>
<h2 style="border: none; padding: 0; margin-top: .5rem;">Heading 2 <small style="color: var(--muted-color); font-size: .5em;">h2</small></h2>
<h3>Heading 3 <small style="color: var(--muted-color); font-size: .6em;">h3</small></h3>
<h4>Heading 4 <small style="color: var(--muted-color); font-size: .6em;">h4</small></h4>
<h5>Heading 5 <small style="color: var(--muted-color); font-size: .6em;">h5</small></h5>
<h6>Heading 6 <small style="color: var(--muted-color); font-size: .6em;">h6</small></h6>
</div>
<p>This is regular body text using <code>--body-color</code> on <code>--body-bg</code>. Font family: <code>--body-font-family</code>. Size: <code>--body-font-size</code> (1rem).</p>
<p><strong>Bold text.</strong> <em>Italic text.</em> <a href="#">This is a link</a>. <code>Inline code</code>. <mark style="background: var(--highlight-bg); color: var(--highlight-color);">Highlighted text</mark>.</p>
<p class="lead">This is lead text styled with <code>--muted-color</code>.</p>
<!-- ══════════════════════════════════════════════
6. LINKS
══════════════════════════════════════════════ -->
<h2>6. Link Colors</h2>
<table class="var-table">
<tr><th>Variable</th><th>Preview</th></tr>
<tr><td><code>--link-color</code></td><td><a href="#" style="color: var(--link-color)">Sample link</a></td></tr>
<tr><td><code>--link-hover-color</code></td><td><span style="color: var(--link-hover-color); text-decoration: underline; cursor: pointer;">Hover state</span></td></tr>
<tr><td><code>--color-link</code></td><td><span style="color: var(--color-link)">color-link value</span></td></tr>
<tr><td><code>--color-hover</code></td><td><span style="color: var(--color-hover)">color-hover value</span></td></tr>
</table>
<!-- ══════════════════════════════════════════════
7. BUTTONS (Bootstrap-style)
══════════════════════════════════════════════ -->
<h2>7. Buttons</h2>
<div style="display: flex; flex-wrap: wrap; gap: .5rem; margin: 1rem 0;">
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-success">Success</button>
<button class="btn btn-danger">Danger</button>
<button class="btn btn-warning">Warning</button>
<button class="btn btn-info">Info</button>
<button class="btn btn-light">Light</button>
<button class="btn btn-dark">Dark</button>
</div>
<div style="display: flex; flex-wrap: wrap; gap: .5rem; margin: 1rem 0;">
<button class="btn btn-outline-primary">Outline Primary</button>
<button class="btn btn-outline-secondary">Outline Secondary</button>
<button class="btn btn-outline-success">Outline Success</button>
<button class="btn btn-outline-danger">Outline Danger</button>
</div>
<!-- ══════════════════════════════════════════════
8. CARDS
══════════════════════════════════════════════ -->
<h2>8. Cards</h2>
<div class="row">
<div class="col">
<div class="card" style="background: var(--card-bg); border: var(--card-border-width) solid var(--card-border-color); border-radius: var(--card-border-radius); padding: 0;">
<div style="padding: var(--card-cap-padding-y) var(--card-cap-padding-x); background: var(--card-cap-bg); color: var(--card-cap-color); border-bottom: 1px solid var(--card-border-color); font-weight: 600;">Card Header</div>
<div style="padding: var(--card-spacer-y) var(--card-spacer-x); color: var(--card-color);">
<h5 style="margin-top: 0;">Card Title</h5>
<p style="margin-bottom: .5rem;">Card body using <code>--card-bg</code>, <code>--card-color</code>, and <code>--card-border-color</code>.</p>
<button class="btn btn-primary" style="font-size: .875rem;">Action</button>
</div>
</div>
</div>
<div class="col">
<div class="card" style="background: var(--card-bg); border: var(--card-border-width) solid var(--card-border-color); border-radius: var(--card-border-radius); padding: var(--card-spacer-y) var(--card-spacer-x); color: var(--card-color);">
<h5 style="margin-top: 0;">Simple Card</h5>
<p>No header, just body content. Uses the same card variables.</p>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════════
9. FORMS
══════════════════════════════════════════════ -->
<h2>9. Form Elements</h2>
<div style="max-width: 480px;">
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: .25rem; font-weight: 500;">Text Input</label>
<input type="text" placeholder="Placeholder text" style="width: 100%; padding: .375rem .75rem; border: 1px solid var(--input-border-color, #ced4da); border-radius: var(--border-radius); background: var(--input-bg, #fff); color: var(--input-color, #22262a); font-size: 1rem;">
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: .25rem; font-weight: 500;">Select</label>
<select style="width: 100%; padding: .375rem .75rem; border: 1px solid var(--input-border-color, #ced4da); border-radius: var(--border-radius); background: var(--input-bg, #fff); color: var(--input-color, #22262a); font-size: 1rem;">
<option>Option 1</option>
<option>Option 2</option>
<option>Option 3</option>
</select>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: .25rem; font-weight: 500;">Textarea</label>
<textarea rows="3" style="width: 100%; padding: .375rem .75rem; border: 1px solid var(--input-border-color, #ced4da); border-radius: var(--border-radius); background: var(--input-bg, #fff); color: var(--input-color, #22262a); font-size: 1rem;">Sample text content</textarea>
</div>
</div>
<!-- ══════════════════════════════════════════════
10. ALERTS (Bootstrap-style)
══════════════════════════════════════════════ -->
<h2>10. Alerts</h2>
<div style="padding: .75rem 1rem; margin-bottom: .75rem; border-radius: var(--border-radius); background: var(--primary-bg-subtle); color: var(--primary-text-emphasis); border: 1px solid var(--primary-border-subtle);">
<strong>Primary alert.</strong> Uses <code>--primary-bg-subtle</code> and <code>--primary-text-emphasis</code>.
</div>
<div style="padding: .75rem 1rem; margin-bottom: .75rem; border-radius: var(--border-radius); background: var(--success-bg-subtle); color: var(--success-text-emphasis); border: 1px solid var(--success-border-subtle);">
<strong>Success alert.</strong> Uses <code>--success-bg-subtle</code> and <code>--success-text-emphasis</code>.
</div>
<div style="padding: .75rem 1rem; margin-bottom: .75rem; border-radius: var(--border-radius); background: var(--warning-bg-subtle); color: var(--warning-text-emphasis); border: 1px solid var(--warning-border-subtle);">
<strong>Warning alert.</strong> Uses <code>--warning-bg-subtle</code> and <code>--warning-text-emphasis</code>.
</div>
<div style="padding: .75rem 1rem; margin-bottom: .75rem; border-radius: var(--border-radius); background: var(--danger-bg-subtle); color: var(--danger-text-emphasis); border: 1px solid var(--danger-border-subtle);">
<strong>Danger alert.</strong> Uses <code>--danger-bg-subtle</code> and <code>--danger-text-emphasis</code>.
</div>
<div style="padding: .75rem 1rem; margin-bottom: .75rem; border-radius: var(--border-radius); background: var(--info-bg-subtle); color: var(--info-text-emphasis); border: 1px solid var(--info-border-subtle);">
<strong>Info alert.</strong> Uses <code>--info-bg-subtle</code> and <code>--info-text-emphasis</code>.
</div>
<!-- ══════════════════════════════════════════════
11. BORDERS & SHADOWS
══════════════════════════════════════════════ -->
<h2>11. Borders &amp; Shadows</h2>
<div class="row">
<div class="col" style="padding: 1.5rem; border: var(--border-width) var(--border-style) var(--border-color); border-radius: var(--border-radius); margin-bottom: 1rem;">
Default border: <code>--border-width</code> / <code>--border-color</code> / <code>--border-radius</code>
</div>
<div class="col" style="padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--box-shadow-sm); margin-bottom: 1rem;">
<code>--box-shadow-sm</code>
</div>
<div class="col" style="padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--box-shadow); margin-bottom: 1rem;">
<code>--box-shadow</code>
</div>
<div class="col" style="padding: 1.5rem; border-radius: var(--border-radius); box-shadow: var(--box-shadow-lg); margin-bottom: 1rem;">
<code>--box-shadow-lg</code>
</div>
</div>
<!-- ══════════════════════════════════════════════
12. NAVIGATION COLORS
══════════════════════════════════════════════ -->
<h2>12. Navigation Colors</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--nav-bg-color)"></div>
<div class="swatch-label"><code>--nav-bg-color</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--nav-text-color); border: 1px solid var(--border-color)"></div>
<div class="swatch-label"><code>--nav-text-color</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--mainmenu-nav-link-color); border: 1px solid var(--border-color)"></div>
<div class="swatch-label"><code>--mainmenu-nav-link-color</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
13. CONTAINER BACKGROUNDS
══════════════════════════════════════════════ -->
<h2>13. Container Background Variables</h2>
<table class="var-table">
<tr><th>Container</th><th>BG Color</th><th>BG Image</th><th>Border</th></tr>
<tr><td>below-topbar</td><td><code>--container-below-topbar-bg-color</code></td><td><code>--container-below-topbar-bg-image</code></td><td><code>--container-below-topbar-border</code></td></tr>
<tr><td>top-a</td><td><code>--container-top-a-bg-color</code></td><td><code>--container-top-a-bg-image</code></td><td><code>--container-top-a-border</code></td></tr>
<tr><td>top-b</td><td><code>--container-top-b-bg-color</code></td><td><code>--container-top-b-bg-image</code></td><td><code>--container-top-b-border</code></td></tr>
<tr><td>bottom-a</td><td><code>--container-bottom-a-bg-color</code></td><td><code>--container-bottom-a-bg-image</code></td><td><code>--container-bottom-a-border</code></td></tr>
<tr><td>bottom-b</td><td><code>--container-bottom-b-bg-color</code></td><td><code>--container-bottom-b-bg-image</code></td><td><code>--container-bottom-b-border</code></td></tr>
<tr><td>sidebar</td><td><code>--container-sidebar-bg-color</code></td><td><code>--container-sidebar-bg-image</code></td><td><code>--container-sidebar-border</code></td></tr>
</table>
<!-- ══════════════════════════════════════════════
14. HERO VARIANTS (NEW)
══════════════════════════════════════════════ -->
<h2>14. Hero Variants <span style="font-size: .65em; color: var(--success); font-weight: normal;">NEW</span></h2>
<p>The <code>.hero#primary</code> and <code>.hero#secondary</code> variants use CSS variables for background color, overlay gradient, and text color. Each adapts automatically with the active theme.</p>
<h3>Primary Variant — <code>.hero#primary</code></h3>
<div class="hero" id="primary" style="background-image: url('data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22400%22 height=%22200%22><rect fill=%22%23a3cde2%22 width=%22400%22 height=%22200%22/><text x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22 font-family=%22sans-serif%22 font-size=%2216%22 fill=%22%23112855%22>Hero Background Image Area</text></svg>'); padding: 0;">
<div style="padding: 3rem 2rem; text-align: center;">
<h2 style="border: none; padding: 0; margin: 0 0 .5rem 0; font-size: 2rem;">Primary Hero</h2>
<p style="margin: 0; font-size: 1.1rem;">Homepage &amp; main landing pages — sky blue tint, softer overlay</p>
</div>
</div>
<h3>Secondary Variant — <code>.hero#secondary</code></h3>
<div class="hero" id="secondary" style="background-image: url('data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%22400%22 height=%22200%22><rect fill=%22%23112855%22 width=%22400%22 height=%22200%22/><text x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22 font-family=%22sans-serif%22 font-size=%2216%22 fill=%22%23f1f5f9%22>Hero Background Image Area</text></svg>'); padding: 0;">
<div style="padding: 3rem 2rem; text-align: center;">
<h2 style="border: none; padding: 0; margin: 0 0 .5rem 0; font-size: 2rem;">Secondary Hero</h2>
<p style="margin: 0; font-size: 1.1rem;">Inner pages, events, about — navy overlay, lighter text</p>
</div>
</div>
<h3>Hero Variable Reference</h3>
<table class="var-table">
<tr><th>Variable</th><th>Variant</th><th>Purpose</th></tr>
<tr><td><code>--hero-primary-bg-color</code></td><td>Primary</td><td>Fallback background color</td></tr>
<tr><td><code>--hero-primary-overlay</code></td><td>Primary</td><td>Gradient overlay tint</td></tr>
<tr><td><code>--hero-primary-color</code></td><td>Primary</td><td>Text color</td></tr>
<tr><td><code>--hero-secondary-bg-color</code></td><td>Secondary</td><td>Fallback background color</td></tr>
<tr><td><code>--hero-secondary-overlay</code></td><td>Secondary</td><td>Gradient overlay tint</td></tr>
<tr><td><code>--hero-secondary-color</code></td><td>Secondary</td><td>Text color</td></tr>
</table>
<!-- ══════════════════════════════════════════════
15. BLOCK COLOR SYSTEM (NEW)
══════════════════════════════════════════════ -->
<h2>15. Block Color System <span style="font-size: .65em; color: var(--success); font-weight: normal;">NEW</span></h2>
<p>Modules in <code>top-a</code>, <code>top-b</code>, <code>bottom-a</code>, and <code>bottom-b</code> positions automatically receive brand colors based on their order. No classes needed — <code>:nth-child()</code> handles assignment.</p>
<h3>Slot Palette Preview</h3>
<div class="block-demo">
<div class="card" style="background-color: var(--block-color-1); color: var(--block-text-1);">
<strong>Slot 1</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-color-1</code>
</div>
<div class="card" style="background-color: var(--block-color-2); color: var(--block-text-2);">
<strong>Slot 2</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-color-2</code>
</div>
<div class="card" style="background-color: var(--block-color-3); color: var(--block-text-3);">
<strong>Slot 3</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-color-3</code>
</div>
<div class="card" style="background-color: var(--block-color-4); color: var(--block-text-4);">
<strong>Slot 4</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-color-4</code>
</div>
</div>
<h3>Named Override Preview</h3>
<div class="block-demo">
<div class="card" style="background-color: var(--block-highlight-bg); color: var(--block-highlight-text);">
<strong>#block-highlight</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-highlight-bg</code>
</div>
<div class="card" style="background-color: var(--block-cta-bg); color: var(--block-cta-text);">
<strong>#block-cta</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-cta-bg</code>
</div>
<div class="card" style="background-color: var(--block-alert-bg); color: var(--block-alert-text);">
<strong>#block-alert</strong><br>
<code style="color: inherit; background: rgba(255,255,255,.15);">--block-alert-bg</code>
</div>
</div>
<h3>Block Variable Reference</h3>
<table class="var-table">
<tr><th>Variable</th><th>Purpose</th></tr>
<tr><td><code>--block-color-1</code> / <code>--block-text-1</code></td><td>1st module in position (automatic)</td></tr>
<tr><td><code>--block-color-2</code> / <code>--block-text-2</code></td><td>2nd module in position (automatic)</td></tr>
<tr><td><code>--block-color-3</code> / <code>--block-text-3</code></td><td>3rd module in position (automatic)</td></tr>
<tr><td><code>--block-color-4</code> / <code>--block-text-4</code></td><td>4th module in position (automatic)</td></tr>
<tr><td><code>--block-highlight-bg</code> / <code>--block-highlight-text</code></td><td>Named override for <code>#block-highlight</code></td></tr>
<tr><td><code>--block-cta-bg</code> / <code>--block-cta-text</code></td><td>Named override for <code>#block-cta</code></td></tr>
<tr><td><code>--block-alert-bg</code> / <code>--block-alert-text</code></td><td>Named override for <code>#block-alert</code></td></tr>
</table>
<h3>Override Priority</h3>
<table class="var-table">
<tr><th>Priority</th><th>Method</th><th>How Applied</th></tr>
<tr><td>1 (highest)</td><td>Named module ID (<code>#block-highlight</code>)</td><td>ID in module HTML + named variable</td></tr>
<tr><td>2 (default)</td><td>Slot color (<code>--block-color-N</code>)</td><td>Automatic by <code>:nth-child()</code> order</td></tr>
</table>
<!-- ══════════════════════════════════════════════
16. VIRTUEMART COLORS
══════════════════════════════════════════════ -->
<h2>16. VirtueMart Surface Colors</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--vm-surface, #fff)"></div>
<div class="swatch-label"><code>--vm-surface</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--vm-surface-2, #f8f9fa)"></div>
<div class="swatch-label"><code>--vm-surface-2</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--vm-price-color, #448344)"></div>
<div class="swatch-label"><code>--vm-price-color</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
17. GABLE COLORS
══════════════════════════════════════════════ -->
<h2>17. Gable Colors</h2>
<div class="swatch-grid">
<div class="swatch">
<div class="swatch-color" style="background: var(--gab-blue)"></div>
<div class="swatch-label"><code>--gab-blue</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gab-green)"></div>
<div class="swatch-label"><code>--gab-green</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gab-red)"></div>
<div class="swatch-label"><code>--gab-red</code></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--gab-orange)"></div>
<div class="swatch-label"><code>--gab-orange</code></div>
</div>
</div>
<!-- ══════════════════════════════════════════════
18. CODE SAMPLES
══════════════════════════════════════════════ -->
<h2>18. Code &amp; Preformatted Text</h2>
<p>Inline code: <code>var(--color-primary)</code></p>
<pre>/* Example: overriding block slot 1 in colors_custom.css */
--block-color-1: var(--accent-color-primary);
--block-text-1: #fff;
/* Hero variant usage in module HTML */
&lt;div class="hero" id="primary"
style="background-image:url('/images/hero/main.jpg')"&gt;
&lt;div class="col-12 py-5 px-4 text-center"&gt;
...content...
&lt;/div&gt;
&lt;/div&gt;</pre>
<!-- ══════════════════════════════════════════════
19. OPACITY UTILITIES
══════════════════════════════════════════════ -->
<h2>19. Opacity Scale</h2>
<div style="display: flex; gap: .5rem; flex-wrap: wrap;">
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-5); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem;">5%</div>
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-10); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem;">10%</div>
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-15); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem;">15%</div>
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-25); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem; color: #fff;">25%</div>
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-50); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem; color: #fff;">50%</div>
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-75); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem; color: #fff;">75%</div>
<div style="width: 60px; height: 60px; background: var(--color-primary); opacity: var(--opacity-100); border-radius: var(--border-radius); display: flex; align-items: center; justify-content: center; font-size: .7rem; color: #fff;">100%</div>
</div>
<!-- ══════════════════════════════════════════════
21. BRANDED BOOTSTRAP 5 SHOWCASE
══════════════════════════════════════════════ -->
<h2>21. Branded Bootstrap 5 Showcase</h2>
<p>Comprehensive component demos using MokoOnyx's brand variables. Mirrors the live Joomla article at <code>/style/branded-bootstrap5</code>.</p>
<!-- BRAND HEADER (gradient + border via variables) -->
<div class="text-center p-4 mb-4" style="background: var(--header-background-image); background-position: center; background-attachment: fixed; background-repeat: repeat; background-size: auto; border-bottom: var(--border, 5px) solid var(--accent-color-primary); color: var(--color-primary);">
<h3 class="mb-1" style="border: none; padding: 0; margin: 0;">Brand + Bootstrap Showcase</h3>
<p class="lead mb-0" style="color: inherit;">Comprehensive components with toggleable code samples</p>
</div>
<!-- NAV SAMPLE (brand colors) -->
<nav class="d-flex align-items-center gap-3 px-3 py-2 mb-4" style="background: var(--nav-bg-color); border-radius: var(--border-radius, .25rem);">
<span class="fw-bold" style="color: var(--nav-text-color);">Brand Nav</span>
<a href="#" class="text-decoration-none" style="color: var(--mainmenu-nav-link-color);">Home</a>
<a href="#" class="text-decoration-none" style="color: var(--mainmenu-nav-link-color);">About</a>
<a href="#" class="text-decoration-none ms-auto fw-semibold" style="color: var(--accent-color-secondary);">Contact</a>
</nav>
<!-- BREADCRUMB -->
<nav aria-label="Breadcrumbs" style="margin-bottom: 1.5rem;">
<ol class="breadcrumb px-3 py-2" style="background: var(--secondary-bg, #eaedf0); border-radius: var(--border-radius, .25rem); margin: 0; list-style: none; display: flex; flex-wrap: wrap; padding: .5rem 1rem; font-size: .875rem;">
<li class="breadcrumb-item"><a href="#">Home</a></li>
<li class="breadcrumb-item"><a href="#">Style</a></li>
<li class="breadcrumb-item active" style="color: var(--muted-color);">Branded Bootstrap5</li>
</ol>
</nav>
<!-- TYPOGRAPHY SECTION -->
<h3>Typography</h3>
<div class="row" style="margin-bottom: 2rem;">
<div class="col">
<h1 style="border: none; padding: 0; margin: .5rem 0;">H1 Heading</h1>
<h2 style="border: none; padding: 0; margin: .5rem 0;">H2 Heading</h2>
<h3 style="margin: .5rem 0;">H3 Heading</h3>
<h4>H4 Heading</h4>
<h5>H5 Heading</h5>
<h6>H6 Heading</h6>
<p class="lead">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.</p>
<p>Curabitur <strong>ullamcorper</strong> nec <em>nisi</em> a <a href="#">themed link</a>. Nulla vitae <code>&lt;section&gt;</code> purus.</p>
<blockquote style="border-left: var(--border, 5px) solid var(--accent-color-primary); padding-left: 1rem; margin: 1rem 0;">
<p style="margin-bottom: .25rem;">"Design is intelligence made visible."</p>
<footer style="color: var(--muted-color); font-size: .875rem;">— Alina Wheeler</footer>
</blockquote>
</div>
</div>
<!-- BUTTONS & GROUPS -->
<h3>Buttons &amp; Button Groups</h3>
<div style="display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1rem;">
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-success">Success</button>
<button class="btn btn-info">Info</button>
<button class="btn btn-warning">Warning</button>
<button class="btn btn-danger">Danger</button>
<button class="btn btn-light">Light</button>
<button class="btn btn-dark">Dark</button>
<button class="btn" style="background: var(--accent-color-primary); color: var(--color-primary); border: none;">Accent</button>
</div>
<div style="display: inline-flex; margin-bottom: 1.5rem;">
<button class="btn btn-outline-primary" style="border-radius: var(--border-radius, .25rem) 0 0 var(--border-radius, .25rem);">Left</button>
<button class="btn btn-outline-primary" style="border-radius: 0;">Middle</button>
<button class="btn btn-outline-primary" style="border-radius: 0 var(--border-radius, .25rem) var(--border-radius, .25rem) 0;">Right</button>
</div>
<!-- BADGES & ALERTS -->
<h3>Badges &amp; Alerts</h3>
<div style="display: flex; flex-wrap: wrap; gap: .35rem; margin-bottom: 1rem;">
<span style="display: inline-block; padding: .25em .5em; font-size: .75em; font-weight: 700; border-radius: var(--border-radius, .25rem); background: var(--primary); color: #fff;">Primary</span>
<span style="display: inline-block; padding: .25em .5em; font-size: .75em; font-weight: 700; border-radius: var(--border-radius, .25rem); background: var(--secondary); color: #fff;">Secondary</span>
<span style="display: inline-block; padding: .25em .5em; font-size: .75em; font-weight: 700; border-radius: var(--border-radius, .25rem); background: var(--success); color: #fff;">Success</span>
<span style="display: inline-block; padding: .25em .5em; font-size: .75em; font-weight: 700; border-radius: var(--border-radius, .25rem); background: var(--warning); color: var(--body-color);">Warning</span>
<span style="display: inline-block; padding: .25em .5em; font-size: .75em; font-weight: 700; border-radius: var(--border-radius, .25rem); background: var(--danger); color: #fff;">Danger</span>
<span style="display: inline-block; padding: .25em .5em; font-size: .75em; font-weight: 700; border-radius: var(--border-radius, .25rem); background: var(--accent-color-primary); color: var(--color-primary);">Accent</span>
</div>
<div style="padding: .75rem 1rem; margin-bottom: .75rem; border-radius: var(--border-radius); background: var(--primary-bg-subtle); color: var(--primary-text-emphasis); border: 1px solid var(--primary-border-subtle);">
<strong>Primary:</strong> Vivamus sagittis lacus vel augue.
</div>
<div style="padding: .75rem 1rem; margin-bottom: .75rem; border-radius: var(--border-radius); background: var(--warning-bg-subtle); color: var(--warning-text-emphasis); border: 1px solid var(--warning-border-subtle);">
Cras mattis consectetur purus sit amet fermentum.
</div>
<div style="padding: .75rem 1rem; margin-bottom: 1.5rem; border-radius: var(--border-radius); background: var(--accent-color-primary); color: var(--color-primary); border: var(--border, 5px) solid var(--accent-color-secondary);">
Brand alert — Aenean lacinia bibendum nulla sed consectetur.
</div>
<!-- TABLES -->
<h3>Tables</h3>
<div style="overflow-x: auto; margin-bottom: 1.5rem;">
<table class="var-table">
<thead><tr style="background: var(--dark, #212529); color: #fff;"><th>#</th><th>Name</th><th>Status</th><th>Notes</th></tr></thead>
<tbody>
<tr><td>1</td><td>Alpha</td><td><span style="display: inline-block; padding: .15em .4em; font-size: .75em; font-weight: 700; border-radius: var(--border-radius); background: var(--success); color: #fff;">Active</span></td><td>Lorem ipsum dolor sit amet.</td></tr>
<tr><td>2</td><td>Beta</td><td><span style="display: inline-block; padding: .15em .4em; font-size: .75em; font-weight: 700; border-radius: var(--border-radius); background: var(--warning); color: var(--body-color);">Pending</span></td><td>Integer posuere erat a ante.</td></tr>
<tr><td>3</td><td>Gamma</td><td><span style="display: inline-block; padding: .15em .4em; font-size: .75em; font-weight: 700; border-radius: var(--border-radius); background: var(--danger); color: #fff;">Blocked</span></td><td>Donec id elit non mi porta.</td></tr>
</tbody>
</table>
</div>
<!-- FORMS (BRANDED) -->
<h3>Branded Forms</h3>
<div style="max-width: 600px; margin-bottom: 1.5rem;">
<div style="display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem;">
<div style="flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: .25rem; font-weight: 500;">Email</label>
<input type="email" placeholder="name@example.com" style="width: 100%; padding: .375rem .75rem; border: 1px solid var(--input-border-color, #ced4da); border-radius: var(--border-radius); background: var(--input-bg, #fff); color: var(--input-color, #22262a);">
</div>
<div style="flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: .25rem; font-weight: 500;">Password</label>
<input type="password" placeholder="••••••••" style="width: 100%; padding: .375rem .75rem; border: 1px solid var(--input-border-color, #ced4da); border-radius: var(--border-radius); background: var(--input-bg, #fff); color: var(--input-color, #22262a);">
</div>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: .25rem; font-weight: 500;">Input Group</label>
<div style="display: flex;">
<span style="display: flex; align-items: center; padding: .375rem .75rem; background: var(--secondary-bg, #eaedf0); border: 1px solid var(--input-border-color, #ced4da); border-right: none; border-radius: var(--border-radius) 0 0 var(--border-radius);">@</span>
<input type="text" placeholder="username" style="flex: 1; padding: .375rem .75rem; border: 1px solid var(--input-border-color, #ced4da); background: var(--input-bg, #fff); color: var(--input-color, #22262a);">
<button class="btn btn-outline-secondary" style="border-radius: 0 var(--border-radius) var(--border-radius) 0;">Search</button>
</div>
</div>
<button class="btn btn-primary">Submit</button>
</div>
<!-- CARDS & LIST GROUPS -->
<h3>Branded Cards &amp; List Groups</h3>
<div class="row" style="margin-bottom: 1.5rem;">
<div class="col">
<div style="border: 1px solid var(--card-border-color, var(--border-color)); border-radius: var(--card-border-radius, var(--border-radius)); overflow: hidden; background: var(--card-bg, var(--body-bg));">
<div style="padding: .5rem 1rem; background: var(--accent-color-primary); color: var(--color-primary); font-weight: 600;">Featured</div>
<div style="padding: var(--card-spacer-y, 1rem) var(--card-spacer-x, 1rem); color: var(--card-color, var(--body-color));">
<h5 style="margin-top: 0;">Card title</h5>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante.</p>
<button class="btn btn-primary" style="font-size: .875rem;">Go somewhere</button>
</div>
</div>
</div>
<div class="col">
<div style="border: 1px solid var(--border-color); border-radius: var(--border-radius); overflow: hidden;">
<div style="padding: .5rem 1rem; background: var(--primary); color: #fff; font-weight: 500;">Active item</div>
<div style="padding: .5rem 1rem; border-bottom: 1px solid var(--border-color);">Second item</div>
<div style="padding: .5rem 1rem; display: flex; justify-content: space-between; align-items: center;">With badge <span style="display: inline-block; padding: .15em .5em; font-size: .75em; font-weight: 700; border-radius: 50rem; background: var(--primary); color: #fff;">4</span></div>
</div>
</div>
</div>
<!-- BREADCRUMB & PAGINATION -->
<h3>Breadcrumb &amp; Pagination</h3>
<nav aria-label="breadcrumb" style="margin-bottom: 1rem;">
<ol style="list-style: none; display: flex; flex-wrap: wrap; padding: .5rem 1rem; margin: 0; background: var(--secondary-bg, #eaedf0); border-radius: var(--border-radius); font-size: .875rem; gap: .5rem;">
<li><a href="#">Home</a> /</li>
<li><a href="#">Library</a> /</li>
<li style="color: var(--muted-color);">Data</li>
</ol>
</nav>
<nav aria-label="Pagination" style="margin-bottom: 1.5rem;">
<ul style="list-style: none; display: flex; padding: 0; margin: 0; gap: 0;">
<li><span style="display: block; padding: .375rem .75rem; border: 1px solid var(--border-color); color: var(--muted-color); border-radius: var(--border-radius) 0 0 var(--border-radius);">Previous</span></li>
<li><a href="#" style="display: block; padding: .375rem .75rem; border: 1px solid var(--border-color); border-left: none; text-decoration: none;">1</a></li>
<li><a href="#" style="display: block; padding: .375rem .75rem; border: 1px solid var(--border-color); border-left: none; text-decoration: none;">2</a></li>
<li><a href="#" style="display: block; padding: .375rem .75rem; border: 1px solid var(--border-color); border-left: none; text-decoration: none;">3</a></li>
<li><a href="#" style="display: block; padding: .375rem .75rem; border: 1px solid var(--border-color); border-left: none; text-decoration: none; border-radius: 0 var(--border-radius) var(--border-radius) 0;">Next</a></li>
</ul>
</nav>
<!-- PROGRESS BARS -->
<h3>Progress Bars</h3>
<div style="margin-bottom: 1.5rem;">
<div style="height: 20px; background: var(--secondary-bg, #eaedf0); border-radius: var(--border-radius); margin-bottom: .5rem; overflow: hidden;">
<div style="width: 25%; height: 100%; background: var(--accent-color-primary); color: var(--color-primary); display: flex; align-items: center; justify-content: center; font-size: .75rem; font-weight: 600;">25%</div>
</div>
<div style="height: 20px; background: var(--secondary-bg, #eaedf0); border-radius: var(--border-radius); overflow: hidden;">
<div style="width: 65%; height: 100%; background: var(--success); color: #fff; display: flex; align-items: center; justify-content: center; font-size: .75rem; font-weight: 600;">65%</div>
</div>
</div>
<!-- CSS VARIABLE SWATCHES (COMPUTED VALUES) -->
<h3>CSS Variable Swatches (Computed)</h3>
<p style="color: var(--muted-color);">Visual preview of key variables with their resolved values displayed via JavaScript.</p>
<div class="swatch-grid" id="computed-swatches">
<div class="swatch">
<div class="swatch-color" style="background: var(--color-primary)"></div>
<div class="swatch-label"><code>--color-primary</code><br><small class="computed-val" data-var="--color-primary"></small></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--color-link)"></div>
<div class="swatch-label"><code>--color-link</code><br><small class="computed-val" data-var="--color-link"></small></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--color-hover)"></div>
<div class="swatch-label"><code>--color-hover</code><br><small class="computed-val" data-var="--color-hover"></small></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--accent-color-primary)"></div>
<div class="swatch-label"><code>--accent-color-primary</code><br><small class="computed-val" data-var="--accent-color-primary"></small></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--accent-color-secondary)"></div>
<div class="swatch-label"><code>--accent-color-secondary</code><br><small class="computed-val" data-var="--accent-color-secondary"></small></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--nav-bg-color)"></div>
<div class="swatch-label"><code>--nav-bg-color</code><br><small class="computed-val" data-var="--nav-bg-color"></small></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--body-bg); border: 1px solid var(--border-color);"></div>
<div class="swatch-label"><code>--body-bg</code><br><small class="computed-val" data-var="--body-bg"></small></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--body-color)"></div>
<div class="swatch-label"><code>--body-color</code><br><small class="computed-val" data-var="--body-color"></small></div>
</div>
<div class="swatch">
<div class="swatch-color" style="background: var(--border-color)"></div>
<div class="swatch-label"><code>--border-color</code><br><small class="computed-val" data-var="--border-color"></small></div>
</div>
</div>
<hr>
<p style="color: var(--muted-color); font-size: .8rem; text-align: center; margin: 2rem 0;">
MokoOnyx Theme Test Sheet &mdash; v03.09.02 &mdash; &copy; 2026 Moko Consulting
</p>
</div><!-- /.test-container -->
<script>
// Populate computed CSS variable values
function updateComputedValues() {
const root = document.documentElement;
const style = getComputedStyle(root);
document.querySelectorAll('.computed-val').forEach(el => {
const varName = el.dataset.var;
if (varName) {
el.textContent = style.getPropertyValue(varName).trim() || '(not set)';
}
});
}
updateComputedValues();
function toggleTheme() {
const html = document.documentElement;
const current = html.getAttribute('data-bs-theme');
const next = current === 'light' ? 'dark' : 'light';
html.setAttribute('data-bs-theme', next);
// Swap stylesheet
const links = document.querySelectorAll('link[rel="stylesheet"]');
links.forEach(link => {
if (link.href.includes('light.custom.css')) {
link.href = link.href.replace('light.custom.css', 'dark.custom.css');
} else if (link.href.includes('dark.custom.css')) {
link.href = link.href.replace('dark.custom.css', 'light.custom.css');
}
});
// Refresh computed values after stylesheet loads
setTimeout(updateComputedValues, 200);
}
</script>
</body>
</html>

View File

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