55 Commits
main ... alpha

Author SHA1 Message Date
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
25 changed files with 1549 additions and 662 deletions

View File

@@ -191,16 +191,6 @@ jobs:
composer install --no-dev --optimize-autoloader --no-interaction composer install --no-dev --optimize-autoloader --no-interaction
fi fi
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Minify CSS and JS
run: |
npm ci --ignore-scripts
node scripts/minify.js
- name: Create package - name: Create package
run: | run: |
mkdir -p build/package mkdir -p build/package
@@ -489,27 +479,39 @@ jobs:
# Push to current branch # Push to current branch
git push || true git push || true
# Sync updates.xml to main via direct API # Also update updates.xml on main via Gitea API (git push blocked by branch protection)
if [ "$CURRENT_BRANCH" != "main" ]; then
GA_TOKEN="${{ secrets.GA_TOKEN }}" GA_TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}" API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# Get current file SHA on main (required for update)
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \ FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty') "${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
if [ -n "$FILE_SHA" ]; then if [ -n "$FILE_SHA" ]; then
# Base64-encode the updates.xml content from working tree (has updated SHA)
CONTENT=$(base64 -w0 updates.xml) CONTENT=$(base64 -w0 updates.xml)
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
"${API}/contents/updates.xml" \ "${API}/contents/updates.xml" \
-d "$(jq -n \ -d "$(jq -n \
--arg content "$CONTENT" \ --arg content "$CONTENT" \
--arg sha "$FILE_SHA" \ --arg sha "$FILE_SHA" \
--arg msg "chore: sync updates.xml ${STABILITY} ${VERSION} [skip ci]" \ --arg msg "chore: update ${STABILITY} channel to ${VERSION} on main [skip ci]" \
--arg branch "main" \ --arg branch "main" \
'{content: $content, sha: $sha, message: $msg, branch: $branch}' '{content: $content, sha: $sha, message: $msg, branch: $branch}'
)" > /dev/null 2>&1 \ )")
&& echo "updates.xml synced to main" \ HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|| echo "WARNING: failed to sync updates.xml to main" if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
echo "updates.xml synced to main via API (HTTP ${HTTP_CODE})"
else
echo "WARNING: failed to sync updates.xml to main (HTTP ${HTTP_CODE})"
echo "$RESPONSE" | head -5
fi
else
echo "WARNING: could not get file SHA for updates.xml on main"
fi
fi fi
- name: Summary - name: Summary

View File

@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Release # INGROUP: MokoStandards.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/joomla/auto-release.yml.template # PATH: /templates/workflows/joomla/auto-release.yml.template
# VERSION: 04.06.00 # VERSION: 04.06.00
# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum # BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum
@@ -22,15 +22,13 @@
# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files | # | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files |
# | 5. Write updates.xml (Joomla update server XML) | # | 5. Write updates.xml (Joomla update server XML) |
# | 6. Create git tag vXX.YY.ZZ | # | 6. Create git tag vXX.YY.ZZ |
# | 7a. Patch: update existing Gitea Release for this minor | # | 7a. Patch: update existing GitHub Release for this minor |
# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml | # | 8. Build ZIP, upload asset, write SHA-256 to updates.xml |
# | | # | |
# | Every version change: archives main -> version/XX.YY branch | # | Every version change: archives main -> version/XX.YY branch |
# | All patches release (including 00). Patch 00/01 = full pipeline. | # | Patch 00 = development (no release). First release = patch 01. |
# | First release only (patch == 01): | # | First release only (patch == 01): |
# | 7b. Create new Gitea Release | # | 7b. Create new GitHub Release |
# | |
# | GitHub mirror: stable/rc releases only (continue-on-error) |
# | | # | |
# +========================================================================+ # +========================================================================+
@@ -48,9 +46,6 @@ on:
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions: permissions:
contents: write contents: write
@@ -66,21 +61,19 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
token: ${{ secrets.GA_TOKEN }} token: ${{ secrets.GA_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Set authenticated push URL
run: git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Setup MokoStandards tools - name: Setup MokoStandards tools
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} GA_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}'
run: | run: |
# Ensure PHP + Composer are available git clone --depth 1 --branch version/04 --quiet \
if ! command -v composer &> /dev/null; then "https://x-access-token:${GH_TOKEN}@git.mokoconsulting.tech/MokoConsulting/MokoStandards-API.git" \
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
fi
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
/tmp/mokostandards-api /tmp/mokostandards-api
cd /tmp/mokostandards-api cd /tmp/mokostandards-api
composer install --no-dev --no-interaction --quiet composer install --no-dev --no-interaction --quiet
@@ -106,16 +99,21 @@ jobs:
echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT" echo "branch=version/${MAJOR}" >> "$GITHUB_OUTPUT"
echo "minor=$MINOR" >> "$GITHUB_OUTPUT" echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
echo "major=$MAJOR" >> "$GITHUB_OUTPUT" echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "release_tag=stable" >> "$GITHUB_OUTPUT" echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
echo "stability=stable" >> "$GITHUB_OUTPUT" if [ "$PATCH" = "00" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch 00 = development — skipping release)"
else
echo "skip=false" >> "$GITHUB_OUTPUT" echo "skip=false" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then if [ "$PATCH" = "01" ]; then
echo "is_minor=true" >> "$GITHUB_OUTPUT" echo "is_minor=true" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (first release for this minor — full pipeline)" echo "Version: $VERSION (first release — full pipeline)"
else else
echo "is_minor=false" >> "$GITHUB_OUTPUT" echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch — platform version + badges only)" echo "Version: $VERSION (patch — platform version + badges only)"
fi fi
fi
- name: Check if already released - name: Check if already released
if: steps.version.outputs.skip != 'true' if: steps.version.outputs.skip != 'true'
@@ -133,8 +131,11 @@ jobs:
echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT" echo "tag_exists=$TAG_EXISTS" >> "$GITHUB_OUTPUT"
echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT" echo "branch_exists=$BRANCH_EXISTS" >> "$GITHUB_OUTPUT"
# Tag and branch may persist across patch releases — never skip if [ "$TAG_EXISTS" = "true" ] && [ "$BRANCH_EXISTS" = "true" ]; then
echo "already_released=true" >> "$GITHUB_OUTPUT"
else
echo "already_released=false" >> "$GITHUB_OUTPUT" echo "already_released=false" >> "$GITHUB_OUTPUT"
fi
# -- SANITY CHECKS ------------------------------------------------------- # -- SANITY CHECKS -------------------------------------------------------
- name: "Sanity: Pre-release validation" - name: "Sanity: Pre-release validation"
@@ -290,15 +291,9 @@ jobs:
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
[ -z "$EXT_TYPE" ] && EXT_TYPE="component" [ -z "$EXT_TYPE" ] && EXT_TYPE="component"
# Derive element if not in manifest: # Templates/modules don't have <element> — derive from <name> (lowercased)
# 1. Try XML filename (e.g. mokowaas.xml → mokowaas)
# 2. Fall back to repo name (lowercased)
if [ -z "$EXT_ELEMENT" ]; then if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
# If filename is generic (templateDetails, manifest), use repo name
case "$EXT_ELEMENT" in
templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
esac
fi fi
# Build client tag: plugins and frontend modules need <client>site</client> # Build client tag: plugins and frontend modules need <client>site</client>
@@ -317,7 +312,7 @@ jobs:
# Build targetplatform (fallback to Joomla 5 if not in manifest) # Build targetplatform (fallback to Joomla 5 if not in manifest)
if [ -z "$TARGET_PLATFORM" ]; then if [ -z "$TARGET_PLATFORM" ]; then
TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/") TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
fi fi
# Build php_minimum tag # Build php_minimum tag
@@ -326,12 +321,11 @@ jobs:
PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>" PHP_TAG="<php_minimum>${PHP_MINIMUM}</php_minimum>"
fi fi
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/stable/${EXT_ELEMENT}-${VERSION}.zip" DOWNLOAD_URL="https://git.mokoconsulting.tech/${{ github.repository }}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip"
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/stable" INFO_URL="https://git.mokoconsulting.tech/${{ github.repository }}/releases/tag/v${VERSION}"
# -- Build update entry for a given stability tag # -- Build stable entry to temp file ─────────────────────────
build_entry() { {
local TAG_NAME="$1"
printf '%s\n' ' <update>' printf '%s\n' ' <update>'
printf '%s\n' " <name>${EXT_NAME}</name>" printf '%s\n' " <name>${EXT_NAME}</name>"
printf '%s\n' " <description>${EXT_NAME} update</description>" printf '%s\n' " <description>${EXT_NAME} update</description>"
@@ -340,7 +334,9 @@ jobs:
printf '%s\n' " <version>${VERSION}</version>" printf '%s\n' " <version>${VERSION}</version>"
[ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}" [ -n "$CLIENT_TAG" ] && printf '%s\n' " ${CLIENT_TAG}"
[ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}" [ -n "$FOLDER_TAG" ] && printf '%s\n' " ${FOLDER_TAG}"
printf '%s\n' " <tags><tag>${TAG_NAME}</tag></tags>" printf '%s\n' ' <tags>'
printf '%s\n' ' <tag>stable</tag>'
printf '%s\n' ' </tags>'
printf '%s\n' " <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>" printf '%s\n' " <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>"
printf '%s\n' ' <downloads>' printf '%s\n' ' <downloads>'
printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>" printf '%s\n' " <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
@@ -350,27 +346,35 @@ jobs:
printf '%s\n' ' <maintainer>Moko Consulting</maintainer>' printf '%s\n' ' <maintainer>Moko Consulting</maintainer>'
printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>' printf '%s\n' ' <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
printf '%s\n' ' </update>' printf '%s\n' ' </update>'
} } > /tmp/stable_entry.xml
# -- Write updates.xml preserving dev/rc entries ──────────────
# Extract existing entries for other stability levels
# Order reflects release workflow: development → alpha → beta → rc → stable
if [ -f "updates.xml" ]; then
printf 'import re, sys\n' > /tmp/extract.py
printf 'with open("updates.xml") as f: c = f.read()\n' >> /tmp/extract.py
printf 'tag = sys.argv[1]\n' >> /tmp/extract.py
printf 'm = re.search(r"( <update>.*?<tag>" + re.escape(tag) + r"</tag>.*?</update>)", c, re.DOTALL)\n' >> /tmp/extract.py
printf 'if m: print(m.group(1))\n' >> /tmp/extract.py
fi
DEV_ENTRY=$(python3 /tmp/extract.py development 2>/dev/null || true)
ALPHA_ENTRY=$(python3 /tmp/extract.py alpha 2>/dev/null || true)
BETA_ENTRY=$(python3 /tmp/extract.py beta 2>/dev/null || true)
RC_ENTRY=$(python3 /tmp/extract.py rc 2>/dev/null || true)
# -- Write updates.xml with cascading channels
# Stable release updates ALL channels (development, alpha, beta, rc, stable)
{ {
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting <hello@mokoconsulting.tech>"
printf '%s\n' " SPDX-License-Identifier: GPL-3.0-or-later"
printf '%s\n' " VERSION: ${VERSION}"
printf '%s\n' " -->"
printf '%s\n' ""
printf '%s\n' '<updates>' printf '%s\n' '<updates>'
build_entry "development" [ -n "$DEV_ENTRY" ] && echo "$DEV_ENTRY"
build_entry "alpha" [ -n "$ALPHA_ENTRY" ] && echo "$ALPHA_ENTRY"
build_entry "beta" [ -n "$BETA_ENTRY" ] && echo "$BETA_ENTRY"
build_entry "rc" [ -n "$RC_ENTRY" ] && echo "$RC_ENTRY"
build_entry "stable" cat /tmp/stable_entry.xml
printf '%s\n' '</updates>' printf '%s\n' '</updates>'
} > updates.xml } > updates.xml
echo "updates.xml: ${VERSION} (all channels updated to stable)" >> $GITHUB_STEP_SUMMARY echo "updates.xml: ${VERSION} (stable + rc/dev preserved)" >> $GITHUB_STEP_SUMMARY
# -- Commit all changes --------------------------------------------------- # -- Commit all changes ---------------------------------------------------
- name: Commit release changes - name: Commit release changes
@@ -385,12 +389,10 @@ jobs:
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]" git config --local user.name "gitea-actions[bot]"
# Set push URL with token for branch-protected repos
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A git add -A
git commit -m "chore(release): build ${VERSION} [skip ci]" \ git commit -m "chore(release): build ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push -u origin HEAD git push
# -- STEP 6: Create tag --------------------------------------------------- # -- STEP 6: Create tag ---------------------------------------------------
- name: "Step 6: Create git tag" - name: "Step 6: Create git tag"
@@ -410,75 +412,69 @@ jobs:
fi fi
echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY echo "Tag: ${TAG}" >> $GITHUB_STEP_SUMMARY
# -- STEP 7: Create or update Gitea Release -------------------------------- # -- STEP 7: Create or update GitHub Release ------------------------------
- name: "Step 7: Gitea Release" - name: "Step 7: GitHub Release"
if: >- if: >-
steps.version.outputs.skip != 'true' steps.version.outputs.skip != 'true' &&
steps.check.outputs.tag_exists != 'true'
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
BRANCH="${{ steps.version.outputs.branch }}" BRANCH="${{ steps.version.outputs.branch }}"
MAJOR="${{ steps.version.outputs.major }}" MAJOR="${{ steps.version.outputs.major }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null) NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}" [ -z "$NOTES" ] && NOTES="Release ${VERSION}"
echo "$NOTES" > /tmp/release_notes.md
# Check if the major release already exists # Check if the major release already exists
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ EXISTING=$(gh release view "$RELEASE_TAG" --json tagName -q .tagName 2>/dev/null || true)
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
EXISTING_ID=$(echo "$EXISTING" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
if [ -z "$EXISTING_ID" ]; then if [ -z "$EXISTING" ]; then
# First release for this major # First release for this major
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ gh release create "$RELEASE_TAG" \
-H "Content-Type: application/json" \ --title "v${MAJOR} (latest: ${VERSION})" \
"${API_BASE}/releases" \ --notes-file /tmp/release_notes.md \
-d "$(python3 -c "import json; print(json.dumps({ --target "$BRANCH"
'tag_name': '${RELEASE_TAG}',
'name': 'v${MAJOR} (latest: ${VERSION})',
'body': '''${NOTES}''',
'target_commitish': '${BRANCH}'
}))")"
echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY echo "Release created: ${RELEASE_TAG} (${VERSION})" >> $GITHUB_STEP_SUMMARY
else else
# Append version notes to existing major release # Append version notes to existing major release
CURRENT_BODY=$(echo "$EXISTING" | python3 -c "import sys,json; print(json.load(sys.stdin).get('body',''))" 2>/dev/null || true) CURRENT_NOTES=$(gh release view "$RELEASE_TAG" --json body -q .body 2>/dev/null || true)
UPDATED_BODY="${CURRENT_BODY} {
echo "$CURRENT_NOTES"
echo ""
echo "---"
echo "### ${VERSION}"
echo ""
cat /tmp/release_notes.md
} > /tmp/updated_notes.md
--- gh release edit "$RELEASE_TAG" \
### ${VERSION} --title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/updated_notes.md
${NOTES}"
curl -sf -X PATCH -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/json" \
"${API_BASE}/releases/${EXISTING_ID}" \
-d "$(python3 -c "import json,sys; print(json.dumps({
'name': 'v${MAJOR} (latest: ${VERSION})',
'body': sys.stdin.read()
}))" <<< "$UPDATED_BODY")"
echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY echo "Release updated: ${RELEASE_TAG} -> ${VERSION}" >> $GITHUB_STEP_SUMMARY
fi fi
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------ # -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
# Every patch builds an install-ready ZIP and uploads it to the minor release.
# Result: one Release per minor version with a ZIP for each patch.
- name: "Step 8: Build Joomla package and update checksum" - name: "Step 8: Build Joomla package and update checksum"
if: >- if: >-
steps.version.outputs.skip != 'true' steps.version.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}" RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
REPO="${{ github.repository }}" REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# All ZIPs upload to the major release tag (vXX) # All ZIPs upload to the major release tag (vXX)
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || {
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -z "$RELEASE_ID" ]; then
echo "No release ${RELEASE_TAG} found — skipping ZIP upload" echo "No release ${RELEASE_TAG} found — skipping ZIP upload"
exit 0 exit 0
fi }
# Find extension element name from manifest # Find extension element name from manifest
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true) MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
@@ -512,109 +508,27 @@ jobs:
SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1) SHA256_ZIP=$(sha256sum "/tmp/${ZIP_NAME}" | cut -d' ' -f1)
SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1) SHA256_TAR=$(sha256sum "/tmp/${TAR_NAME}" | cut -d' ' -f1)
# -- Delete existing assets with same name before uploading ------
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
for ASSET_NAME in "$ZIP_NAME" "$TAR_NAME"; do
ASSET_ID=$(echo "$ASSETS" | python3 -c "
import sys,json
assets = json.load(sys.stdin)
for a in assets:
if a['name'] == '${ASSET_NAME}':
print(a['id']); break
" 2>/dev/null || true)
if [ -n "$ASSET_ID" ]; then
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
fi
done
# -- Upload both to release tag ---------------------------------- # -- Upload both to release tag ----------------------------------
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ gh release upload "$RELEASE_TAG" "/tmp/${ZIP_NAME}" --clobber 2>/dev/null || true
-H "Content-Type: application/octet-stream" \ gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true
--data-binary @"/tmp/${ZIP_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" > /dev/null 2>&1 || true
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${TAR_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
# -- Update updates.xml with both download formats --------------- # -- Update updates.xml with both download formats ---------------
if [ -f "updates.xml" ]; then if [ -f "updates.xml" ]; then
ZIP_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}" ZIP_URL="https://git.mokoconsulting.tech/${{ github.repository }}/releases/download/${RELEASE_TAG}/${ZIP_NAME}"
TAR_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${TAR_NAME}" TAR_URL="https://git.mokoconsulting.tech/${{ github.repository }}/releases/download/${RELEASE_TAG}/${TAR_NAME}"
# Use Python to update only the stable entry's downloads + sha256 # Replace downloads block with both formats + SHA
export PY_ZIP_URL="$ZIP_URL" PY_TAR_URL="$TAR_URL" PY_SHA="$SHA256_ZIP" sed -i "s|<downloads>.*</downloads>|<downloads>\n <downloadurl type=\"full\" format=\"zip\">${ZIP_URL}</downloadurl>\n <downloadurl type=\"full\" format=\"tar.gz\">${TAR_URL}</downloadurl>\n </downloads>|" updates.xml 2>/dev/null || true
python3 << 'PYEOF' if grep -q '<sha256>' updates.xml; then
import re, os sed -i "s|<sha256>.*</sha256>|<sha256>${SHA256_ZIP}</sha256>|" updates.xml
else
sed -i "s|</downloads>|</downloads>\n <sha256>${SHA256_ZIP}</sha256>|" updates.xml
fi
with open("updates.xml") as f:
content = f.read()
zip_url = os.environ["PY_ZIP_URL"]
tar_url = os.environ["PY_TAR_URL"]
sha = os.environ["PY_SHA"]
# Find the stable update block and replace its downloads + sha256
def replace_stable(m):
block = m.group(0)
# Replace downloads block
new_downloads = (
" <downloads>\n"
f" <downloadurl type=\"full\" format=\"zip\">{zip_url}</downloadurl>\n"
" </downloads>"
)
block = re.sub(r' <downloads>.*?</downloads>', new_downloads, block, flags=re.DOTALL)
# Add or replace sha256
if '<sha256>' in block:
block = re.sub(r' <sha256>.*?</sha256>', f' <sha256>{sha}</sha256>', block)
else:
block = block.replace('</downloads>', f'</downloads>\n <sha256>{sha}</sha256>')
return block
content = re.sub(
r' <update>.*?<tag>stable</tag>.*?</update>',
replace_stable,
content,
flags=re.DOTALL
)
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
CURRENT_BRANCH="${{ github.ref_name }}"
git add updates.xml git add updates.xml
git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \ git commit -m "chore(release): ZIP + tar.gz for ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" || true --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" || true
git push || true git push || true
# Sync updates.xml to main via direct API (always runs — may be on version/XX branch)
GA_TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL:-https://git.mokoconsulting.tech}/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 ${VERSION} [skip ci]" \
--arg branch "main" \
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
)" > /dev/null 2>&1 \
&& echo "updates.xml synced to main via API" \
|| echo "WARNING: failed to sync updates.xml to main"
else
echo "WARNING: could not get updates.xml SHA from main"
fi
fi fi
echo "### Joomla Packages" >> $GITHUB_STEP_SUMMARY echo "### Joomla Packages" >> $GITHUB_STEP_SUMMARY
@@ -624,50 +538,7 @@ jobs:
echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY echo "| \`${ZIP_NAME}\` | ${ZIP_SIZE} | \`${SHA256_ZIP}\` |" >> $GITHUB_STEP_SUMMARY
echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY echo "| \`${TAR_NAME}\` | ${TAR_SIZE} | \`${SHA256_TAR}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY echo "| Release | \`${RELEASE_TAG}\` | |" >> $GITHUB_STEP_SUMMARY
echo "| Download | [${ZIP_NAME}](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${ZIP_NAME}) |" >> $GITHUB_STEP_SUMMARY echo "| Download | [${PACKAGE_NAME}](https://git.mokoconsulting.tech/${{ github.repository }}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}) |" >> $GITHUB_STEP_SUMMARY
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
steps.version.outputs.stability == 'stable' &&
secrets.GH_TOKEN != ''
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
MAJOR="${{ steps.version.outputs.major }}"
BRANCH="${{ steps.version.outputs.branch }}"
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
NOTES=$(php /tmp/mokostandards-api/cli/release_notes.php --path . --version "$VERSION" 2>/dev/null || true)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
echo "$NOTES" > /tmp/release_notes.md
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".tag_name // empty" || true)
if [ -z "$EXISTING" ]; then
gh release create "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" \
--notes-file /tmp/release_notes.md \
--target "$BRANCH" || true
else
gh release edit "$RELEASE_TAG" \
--repo "$GH_REPO" \
--title "v${MAJOR} (latest: ${VERSION})" || true
fi
# Upload assets to GitHub mirror
for PKG in /tmp/${EXT_ELEMENT:-pkg}-${VERSION}.*; do
if [ -f "$PKG" ]; then
_RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" 2>/dev/null | jq -r ".id // empty")
[ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
fi
done
echo "GitHub mirror updated: ${GH_REPO} ${RELEASE_TAG}" >> $GITHUB_STEP_SUMMARY
# -- Summary -------------------------------------------------------------- # -- Summary --------------------------------------------------------------
- name: Pipeline Summary - name: Pipeline Summary
@@ -688,5 +559,5 @@ jobs:
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY echo "| Release | [View](https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi fi

View File

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

View File

@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: Gitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Joomla # INGROUP: MokoStandards.Joomla
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/workflows/joomla/update-server.yml.template # PATH: /templates/workflows/joomla/update-server.yml.template
# VERSION: 04.06.00 # VERSION: 04.06.00
# BRIEF: Update Joomla update server XML feed with stable/rc/dev entries # BRIEF: Update Joomla update server XML feed with stable/rc/dev entries
@@ -13,27 +13,16 @@
# Writes updates.xml with multiple <update> entries: # Writes updates.xml with multiple <update> entries:
# - <tag>stable</tag> on push to main (from auto-release) # - <tag>stable</tag> on push to main (from auto-release)
# - <tag>rc</tag> on push to rc/** # - <tag>rc</tag> on push to rc/**
# - <tag>development</tag> on push to dev or dev/** # - <tag>development</tag> on push to dev/**
# #
# Joomla filters by user's "Minimum Stability" setting. # Joomla filters by user's "Minimum Stability" setting.
name: Update Joomla Update Server XML Feed name: Update Joomla Update Server XML Feed
on: on:
push:
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
pull_request: pull_request:
types: [closed] types: [closed]
branches: branches:
- 'dev'
- 'dev/**' - 'dev/**'
- 'alpha/**' - 'alpha/**'
- 'beta/**' - 'beta/**'
@@ -57,9 +46,6 @@ on:
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions: permissions:
contents: write contents: write
@@ -69,50 +55,46 @@ jobs:
name: Update updates.xml name: Update updates.xml
runs-on: release runs-on: release
if: >- if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push' github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with: with:
token: ${{ secrets.GA_TOKEN }} token: ${{ secrets.GA_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Setup MokoStandards tools - name: Setup MokoStandards tools
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }} GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN }}"}}'
run: | run: |
if ! command -v composer &> /dev/null; then git clone --depth 1 --branch version/04 --quiet \
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 "https://x-access-token:${GH_TOKEN}@git.mokoconsulting.tech/MokoConsulting/MokoStandards.git" \
fi /tmp/mokostandards 2>/dev/null || true
git clone --depth 1 --branch main --quiet \ if [ -d "/tmp/mokostandards" ] && [ -f "/tmp/mokostandards/composer.json" ]; then
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \ cd /tmp/mokostandards && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
/tmp/mokostandards-api 2>/dev/null || true
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi fi
- name: Generate updates.xml entry - name: Generate updates.xml entry
id: update
run: | run: |
BRANCH="${{ github.ref_name }}" BRANCH="${{ github.ref_name }}"
REPO="${{ github.repository }}" REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Auto-bump patch on all branches (dev, alpha, beta, rc) # Auto-bump patch on alpha/beta/rc branches (not dev — dev bumps manually)
if [[ "$BRANCH" != dev/* ]]; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]" git config --local user.name "gitea-actions[bot]"
BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true) BUMPED=$(php /tmp/mokostandards/api/cli/version_bump.php --path . 2>/dev/null || true)
if [ -n "$BUMPED" ]; then if [ -n "$BUMPED" ]; then
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION") VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
git add -A git add -A
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \ git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true
git push 2>/dev/null || true git push 2>/dev/null || true
fi fi
fi
# Determine stability from branch or input # Determine stability from branch or input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
@@ -123,14 +105,12 @@ jobs:
STABILITY="beta" STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha" STABILITY="alpha"
elif [[ "$BRANCH" == dev/* ]] || [[ "$BRANCH" == "dev" ]]; then elif [[ "$BRANCH" == dev/* ]]; then
STABILITY="development" STABILITY="development"
else else
STABILITY="stable" STABILITY="stable"
fi fi
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
# Parse manifest (portable — no grep -P) # Parse manifest (portable — no grep -P)
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then if [ -z "$MANIFEST" ]; then
@@ -152,18 +132,15 @@ jobs:
[ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}" [ -z "$EXT_NAME" ] && EXT_NAME="${{ github.event.repository.name }}"
[ -z "$EXT_TYPE" ] && EXT_TYPE="component" [ -z "$EXT_TYPE" ] && EXT_TYPE="component"
# Derive element if not in manifest: try XML filename, then repo name # Templates and modules don't have <element> — derive from <name>
if [ -z "$EXT_ELEMENT" ]; then if [ -z "$EXT_ELEMENT" ]; then
EXT_ELEMENT=$(basename "$MANIFEST" .xml | tr '[:upper:]' '[:lower:]') EXT_ELEMENT=$(echo "$EXT_NAME" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
case "$EXT_ELEMENT" in
templatedetails|manifest|*.xml) EXT_ELEMENT=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]' | tr -d ' -') ;;
esac
fi fi
# Use manifest version if README version is empty # Use manifest version if README version is empty
[ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION" [ "$VERSION" = "0.0.0" ] && [ -n "$EXT_VERSION" ] && VERSION="$EXT_VERSION"
[ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="((5.[0-9])|(6.[0-9]))" %s>' "/") [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")
CLIENT_TAG="" CLIENT_TAG=""
[ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>" [ -n "$EXT_CLIENT" ] && CLIENT_TAG="<client>${EXT_CLIENT}</client>"
@@ -196,10 +173,10 @@ jobs:
esac esac
PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip" PACKAGE_NAME="${EXT_ELEMENT}-${DISPLAY_VERSION}.zip"
DOWNLOAD_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}" DOWNLOAD_URL="https://git.mokoconsulting.tech/${{ github.repository }}/releases/download/${RELEASE_TAG}/${PACKAGE_NAME}"
INFO_URL="${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}" INFO_URL="https://github.com/${REPO}"
# -- Build install packages (ZIP + tar.gz) -------------------- # ── Build install packages (ZIP + tar.gz) ───────────────────
SOURCE_DIR="src" SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs" [ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ -d "$SOURCE_DIR" ]; then if [ -d "$SOURCE_DIR" ]; then
@@ -215,62 +192,20 @@ jobs:
SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1) SHA256=$(sha256sum "/tmp/${PACKAGE_NAME}" | cut -d' ' -f1)
# Ensure release exists on Gitea # Ensure release exists
RELEASE_JSON=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ gh release view "$RELEASE_TAG" --json tagName > /dev/null 2>&1 || \
"${API_BASE}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true) gh release create "$RELEASE_TAG" --title "${RELEASE_TAG} (${DISPLAY_VERSION})" --notes "${STABILITY} release" --prerelease --target main 2>/dev/null || true
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -z "$RELEASE_ID" ]; then
# Create release
RELEASE_JSON=$(curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/json" \
"${API_BASE}/releases" \
-d "$(python3 -c "import json; print(json.dumps({
'tag_name': '${RELEASE_TAG}',
'name': '${RELEASE_TAG} (${DISPLAY_VERSION})',
'body': '${STABILITY} release',
'prerelease': True,
'target_commitish': 'main'
}))")" 2>/dev/null || true)
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
fi
if [ -n "$RELEASE_ID" ]; then
# Delete existing assets with same name before uploading
ASSETS=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets" 2>/dev/null || echo "[]")
for ASSET_FILE in "$PACKAGE_NAME" "$TAR_NAME"; do
ASSET_ID=$(echo "$ASSETS" | python3 -c "
import sys,json
assets = json.load(sys.stdin)
for a in assets:
if a['name'] == '${ASSET_FILE}':
print(a['id']); break
" 2>/dev/null || true)
if [ -n "$ASSET_ID" ]; then
curl -sf -X DELETE -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
"${API_BASE}/releases/${RELEASE_ID}/assets/${ASSET_ID}" 2>/dev/null || true
fi
done
# Upload both formats # Upload both formats
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ gh release upload "$RELEASE_TAG" "/tmp/${PACKAGE_NAME}" --clobber 2>/dev/null || true
-H "Content-Type: application/octet-stream" \ gh release upload "$RELEASE_TAG" "/tmp/${TAR_NAME}" --clobber 2>/dev/null || true
--data-binary @"/tmp/${PACKAGE_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${PACKAGE_NAME}" > /dev/null 2>&1 || true
curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"/tmp/${TAR_NAME}" \
"${API_BASE}/releases/${RELEASE_ID}/assets?name=${TAR_NAME}" > /dev/null 2>&1 || true
fi
echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY echo "Packages: ${PACKAGE_NAME} + ${TAR_NAME} (SHA: ${SHA256})" >> $GITHUB_STEP_SUMMARY
else else
SHA256="" SHA256=""
fi fi
# -- Build the new entry ----------------------------------------- # ── Build the new entry ───────────────────────────────────────
NEW_ENTRY="" NEW_ENTRY=""
NEW_ENTRY="${NEW_ENTRY} <update>\n" NEW_ENTRY="${NEW_ENTRY} <update>\n"
NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n" NEW_ENTRY="${NEW_ENTRY} <name>${EXT_NAME}</name>\n"
@@ -285,76 +220,40 @@ jobs:
NEW_ENTRY="${NEW_ENTRY} </tags>\n" NEW_ENTRY="${NEW_ENTRY} </tags>\n"
NEW_ENTRY="${NEW_ENTRY} <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>\n" NEW_ENTRY="${NEW_ENTRY} <infourl title=\"${EXT_NAME}\">${INFO_URL}</infourl>\n"
NEW_ENTRY="${NEW_ENTRY} <downloads>\n" NEW_ENTRY="${NEW_ENTRY} <downloads>\n"
TAR_URL="https://git.mokoconsulting.tech/${{ github.repository }}/releases/download/${RELEASE_TAG}/${EXT_ELEMENT}-${DISPLAY_VERSION}.tar.gz"
NEW_ENTRY="${NEW_ENTRY} <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>\n" NEW_ENTRY="${NEW_ENTRY} <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>\n"
NEW_ENTRY="${NEW_ENTRY} <downloadurl type=\"full\" format=\"tar.gz\">${TAR_URL}</downloadurl>\n"
NEW_ENTRY="${NEW_ENTRY} </downloads>\n" NEW_ENTRY="${NEW_ENTRY} </downloads>\n"
[ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>${SHA256}</sha256>\n" [ -n "$SHA256" ] && NEW_ENTRY="${NEW_ENTRY} <sha256>sha256:${SHA256}</sha256>\n"
NEW_ENTRY="${NEW_ENTRY} ${TARGET_PLATFORM}\n" NEW_ENTRY="${NEW_ENTRY} ${TARGET_PLATFORM}\n"
[ -n "$PHP_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_TAG}\n" [ -n "$PHP_TAG" ] && NEW_ENTRY="${NEW_ENTRY} ${PHP_TAG}\n"
NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n" NEW_ENTRY="${NEW_ENTRY} <maintainer>Moko Consulting</maintainer>\n"
NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n" NEW_ENTRY="${NEW_ENTRY} <maintainerurl>https://mokoconsulting.tech</maintainerurl>\n"
NEW_ENTRY="${NEW_ENTRY} </update>" NEW_ENTRY="${NEW_ENTRY} </update>"
# -- Write new entry to temp file -------------------------------- # ── Write new entry to temp file ───────────────────────────────
printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml printf '%b' "$NEW_ENTRY" > /tmp/new_entry.xml
# -- Merge into updates.xml (only update this stability channel) - # ── Merge into updates.xml ─────────────────────────────────────
# Cascading update: each stability level updates itself and all lower levels
# stable → all | rc → rc,beta,alpha,dev | beta → beta,alpha,dev | alpha → alpha,dev | dev → dev
CASCADE_MAP="stable:development,alpha,beta,rc,stable rc:development,alpha,beta,rc beta:development,alpha,beta alpha:development,alpha development:development"
TARGETS=""
for entry in $CASCADE_MAP; do
key="${entry%%:*}"
vals="${entry#*:}"
if [ "$key" = "${STABILITY}" ]; then
TARGETS="$vals"
break
fi
done
[ -z "$TARGETS" ] && TARGETS="${STABILITY}"
if [ ! -f "updates.xml" ]; then if [ ! -f "updates.xml" ]; then
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" > updates.xml printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>' > updates.xml
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting <hello@mokoconsulting.tech>" >> updates.xml
printf '%s\n' " SPDX-License-Identifier: GPL-3.0-or-later" >> updates.xml
printf '%s\n' " VERSION: ${VERSION}" >> updates.xml
printf '%s\n' " -->" >> updates.xml
printf '%s\n' "" >> updates.xml
printf '%s\n' '<updates>' >> updates.xml printf '%s\n' '<updates>' >> updates.xml
cat /tmp/new_entry.xml >> updates.xml cat /tmp/new_entry.xml >> updates.xml
printf '\n%s\n' '</updates>' >> updates.xml printf '\n%s\n' '</updates>' >> updates.xml
else else
# Replace each cascading channel with the new entry (different tag) # Remove existing entry for this stability, insert new one
export PY_TARGETS="$TARGETS" printf 'import re\nstability = "%s"\n' "${STABILITY}" > /tmp/merge_xml.py
python3 << PYEOF printf 'with open("updates.xml") as f: content = f.read()\n' >> /tmp/merge_xml.py
import re, os printf 'with open("/tmp/new_entry.xml") as f: new_entry = f.read()\n' >> /tmp/merge_xml.py
targets = os.environ["PY_TARGETS"].split(",") printf 'pattern = r" <update>.*?<tag>" + re.escape(stability) + r"</tag>.*?</update>\\n?"\n' >> /tmp/merge_xml.py
stability = "${STABILITY}" printf 'content = re.sub(pattern, "", content, flags=re.DOTALL)\n' >> /tmp/merge_xml.py
with open("updates.xml") as f: printf 'content = content.replace("</updates>", new_entry + "\\n</updates>")\n' >> /tmp/merge_xml.py
content = f.read() printf 'content = re.sub(r"\\n{3,}", "\\n\\n", content)\n' >> /tmp/merge_xml.py
with open("/tmp/new_entry.xml") as f: printf 'with open("updates.xml", "w") as f: f.write(content)\n' >> /tmp/merge_xml.py
new_entry_template = f.read() python3 /tmp/merge_xml.py 2>/dev/null || {
for tag in targets:
tag = tag.strip()
# Build entry with this tag
new_entry = re.sub(r"<tag>[^<]*</tag>", f"<tag>{tag}</tag>", new_entry_template)
# Remove existing entry for this tag
pattern = r" <update>.*?<tag>" + re.escape(tag) + r"</tag>.*?</update>\n?"
content = re.sub(pattern, "", content, flags=re.DOTALL)
# Insert before </updates>
content = content.replace("</updates>", new_entry + "\n</updates>")
content = re.sub(r"\n{3,}", "\n\n", content)
with open("updates.xml", "w") as f:
f.write(content)
PYEOF
if [ $? -ne 0 ]; then
# Fallback: rebuild keeping other stability entries # Fallback: rebuild keeping other stability entries
{ {
printf '%s\n' "<?xml version='1.0' encoding='UTF-8'?>" printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
printf '%s\n' "<!-- Copyright (C) $(date +%Y) Moko Consulting <hello@mokoconsulting.tech>"
printf '%s\n' " SPDX-License-Identifier: GPL-3.0-or-later"
printf '%s\n' " VERSION: ${VERSION}"
printf '%s\n' " -->"
printf '%s\n' ""
printf '%s\n' '<updates>' printf '%s\n' '<updates>'
for TAG in stable rc development; do for TAG in stable rc development; do
[ "$TAG" = "${STABILITY}" ] && continue [ "$TAG" = "${STABILITY}" ] && continue
@@ -366,7 +265,7 @@ jobs:
printf '\n%s\n' '</updates>' printf '\n%s\n' '</updates>'
} > /tmp/updates_new.xml } > /tmp/updates_new.xml
mv /tmp/updates_new.xml updates.xml mv /tmp/updates_new.xml updates.xml
fi }
fi fi
# Commit # Commit
@@ -379,55 +278,8 @@ jobs:
git push git push
} }
# -- Sync updates.xml to main (for non-main branches) ----------------------
- name: Sync updates.xml to main
if: github.ref_name != 'main'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GA_TOKEN="${{ secrets.GA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
CONTENT=$(base64 -w0 updates.xml)
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/contents/updates.xml" \
-d "$(python3 -c "import json; print(json.dumps({
'content': '${CONTENT}',
'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${STABILITY} [skip ci]',
'branch': 'main'
}))")" > /dev/null 2>&1 \
&& echo "updates.xml synced to main (${STABILITY})" >> $GITHUB_STEP_SUMMARY \
|| echo "WARNING: failed to sync updates.xml to main" >> $GITHUB_STEP_SUMMARY
else
echo "WARNING: could not get updates.xml SHA from main" >> $GITHUB_STEP_SUMMARY
fi
# -- Mirror to GitHub (stable and rc only) --------------------------------
- name: Mirror release to GitHub
if: >-
(steps.update.outputs.stability == 'stable' || steps.update.outputs.stability == 'rc') &&
secrets.GH_TOKEN != ''
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
STABILITY="${{ steps.update.outputs.stability }}"
echo "GitHub mirror sync for ${STABILITY} — ${GH_REPO}" >> $GITHUB_STEP_SUMMARY
# Mirror packages if they exist
for PKG in /tmp/*.zip /tmp/*.tar.gz; do
if [ -f "$PKG" ]; then
_RELID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/tags/${RELEASE_TAG}" 2>/dev/null | jq -r ".id // empty")
[ -n "$_RELID" ] && curl -sf -X POST -H "Authorization: token ${{ secrets.GA_TOKEN }}" -H "Content-Type: application/octet-stream" "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${{ github.repository }}/releases/${_RELID}/assets?name=$(basename $PKG)" --data-binary "@$PKG" > /dev/null 2>&1 || true
fi
done
- name: SFTP deploy to dev server - name: SFTP deploy to dev server
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev' if: contains(github.ref, 'dev/')
env: env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }} DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }} DEV_PATH: ${{ vars.DEV_FTP_PATH }}
@@ -436,15 +288,15 @@ jobs:
DEV_PORT: ${{ vars.DEV_FTP_PORT }} DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }} DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }} DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: | run: |
# -- Permission check: admin or maintain role required -------- # ── Permission check: admin or maintain role required ──────
ACTOR="${{ github.actor }}" ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}" REPO="${{ github.repository }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/collaborators/${ACTOR}/permission" 2>/dev/null \
2>/dev/null | jq -r '.permission' || \
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" \ curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/collaborators/${ACTOR}" 2>/dev/null \
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \ 2>/dev/null | jq -r '.role' || echo "read")
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
case "$PERMISSION" in case "$PERMISSION" in
admin|maintain|write) ;; admin|maintain|write) ;;
*) *)
@@ -472,11 +324,11 @@ jobs:
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi fi
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true) PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json php /tmp/mokostandards/api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then elif [ -f "/tmp/mokostandards/api/deploy/deploy-sftp.php" ]; then
php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json php /tmp/mokostandards/api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi fi
rm -f /tmp/deploy_key /tmp/sftp-config.json rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY

8
.gitignore vendored
View File

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

496
README.md
View File

@@ -9,7 +9,7 @@
INGROUP: MokoOnyx.Documentation INGROUP: MokoOnyx.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
FILE: ./README.md FILE: ./README.md
VERSION: 01.00.19 VERSION: 01.00.17
BRIEF: Documentation for MokoOnyx template BRIEF: Documentation for MokoOnyx template
--> -->
@@ -24,31 +24,489 @@
[![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org) [![Joomla](https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&logoColor=white)](https://www.joomla.org)
[![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net) [![PHP](https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&logoColor=white)](https://www.php.net)
MokoOnyx is a modern, lightweight enhancement layer built on top of Joomla's Cassiopeia template. It adds **Font Awesome 7**, **Bootstrap 5** helpers, an automatic **Table of Contents (TOC)** utility, advanced **Dark Mode** theming, and optional integrations for **Google Tag Manager** and **Google Analytics (GA4)** -- all while maintaining minimal core template overrides for maximum upgrade compatibility. MokoOnyx is a modern, lightweight enhancement layer built on top of Joomla's Cassiopeia template. It adds **Font Awesome 7**, **Bootstrap 5** helpers, an automatic **Table of Contents (TOC)** utility, advanced **Dark Mode** theming, and optional integrations for **Google Tag Manager** and **Google Analytics (GA4)**all while maintaining minimal core template overrides for maximum upgrade compatibility.
## Features ---
## 📑 Table of Contents
- [Features](#-features)
- [Requirements](#-requirements)
- [Installation](#-installation)
- [Quick Start](#-quick-start)
- [Configuration](#-configuration)
- [Theme System](#-theme-system)
- [Development](#-development)
- [Documentation](#-documentation)
- [Changelog](#-changelog)
- [Support](#-support)
- [Contributing](#-contributing)
- [Included Libraries](#-included-libraries)
- [License](#-license)
---
## ✨ Features
### Core Enhancements
- **Built on Cassiopeia**: Extends Joomla's default template with minimal overrides - **Built on Cassiopeia**: Extends Joomla's default template with minimal overrides
- **Font Awesome 7**: Fully integrated into Joomla's asset manager with 2,000+ icons - **Font Awesome 7**: Fully integrated into Joomla's asset manager with 2,000+ icons
- **Bootstrap 5**: Extended utility classes and responsive grid system - **Bootstrap 5**: Extended utility classes and responsive grid system
- **Template Overrides**: Includes overrides for all core Joomla modules, Community Builder, and DPCalendar - **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
- **Dark Mode Support**: Built-in light/dark mode toggle with system preference detection - **Dark Mode Support**: Built-in light/dark mode toggle with system preference detection
- **Google Tag Manager / GA4**: Optional analytics integrations - **Color Palettes**: Standard, Alternative, and Custom color schemes
- **Theme Persistence**: User preferences saved via localStorage
- **Theme Control Options**: Switch, radio buttons, or hidden controls
- **Auto Dark Mode**: Optional automatic dark mode based on time/system settings
- **Meta Tags**: Automatic color-scheme and theme-color meta tags
### Developer Features
- **Custom Code Injection**: Add custom HTML to `<head>` start/end
- **Drawer Sidebars**: Configurable left/right drawer positions with custom icons
- **Font Options**: Local and web fonts (Roboto, Fira Sans, Noto Sans)
- **Sticky Header**: Optional sticky navigation
- **Back to Top**: Floating back-to-top button
### Analytics & Tracking
- **Google Tag Manager**: Optional GTM integration with container ID configuration
- **Google Analytics**: Optional GA4 integration with measurement ID
- **Privacy-Friendly**: All tracking features are optional and easily disabled
### Content Features
- **Table of Contents**: Automatic TOC generation for long articles - **Table of Contents**: Automatic TOC generation for long articles
- Placement options: `toc-left` or `toc-right` layouts
## Requirements - Automatic heading extraction and navigation
- Responsive sidebar positioning
- **Joomla**: 5.x or 6.x
- **PHP**: 8.1 or higher
## Installation
Download the latest `mokoonyx-{version}.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases) and install via Joomla's Extension Manager.
## License
This project is licensed under the **GNU General Public License v3.0** - see the [LICENSE](./LICENSE) file for details.
--- ---
**Made with love by [Moko Consulting](https://mokoconsulting.tech)** ## 📋 Requirements
- **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)
---
## 📦 Installation
**Note**: MokoOnyx is a **standalone Joomla template extension** (not bundled as a package). Install it directly via Joomla's Extension Manager.
### Via Joomla Extension Manager
1. Download the latest `mokoonyx-{version}.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases)
2. In Joomla admin, navigate to **System → Install → Extensions**
3. Upload the ZIP file and click **Upload & Install**
4. Navigate to **System → Site Templates**
5. Set **MokoOnyx** as your default template
### Via Git (Development)
```bash
git clone https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git
cd MokoOnyx
```
See [Development Guide](./docs/JOOMLA_DEVELOPMENT.md) for development setup.
---
## 🚀 Quick Start
### 1. Install the Template
Install `mokoonyx.zip` via Joomla's Extension Manager.
### 2. Set as Default
Navigate to **System → Site Templates** and set **MokoOnyx** as default.
### 3. Configure Template Options
Go to **System → Site Templates → MokoOnyx** to configure:
- **Branding**: Upload logo, set site title/description
- **Theme**: Configure color schemes and dark mode
- **Layout**: Set container type (static/fluid), sticky header
- **Analytics**: Add GTM/GA4 tracking codes (optional)
- **Custom Code**: Inject custom HTML/CSS/JS
### 4. Test Dark Mode
The template includes a dark mode toggle. Test it by:
- Using the floating theme toggle button (bottom-right by default)
- Checking theme persistence across page loads
- Verifying system preference detection
---
## Usage
Once installed and set as the default site template, MokoOnyx works out of the box with Joomla's standard content and module system. Key usage points:
- **Template Options** — Configure via **System → Site Templates → MokoOnyx** (theme colours, layout, analytics, favicon, drawers)
- **Custom Colour Schemes** — Copy `templates/mokoonyx/templates/light.custom.css` or `dark.custom.css` to `media/templates/site/mokoonyx/css/theme/` and select "Custom" in the Theme tab
- **Custom CSS/JS** — Create `media/templates/site/mokoonyx/css/user.css` or `js/user.js` for site-specific overrides that survive template updates
- **Module Overrides** — The template includes overrides for common Joomla modules with consistent title rendering, Bootstrap 5 styling, and Font Awesome 7 icons
- **Dark Mode** — Enabled by default with a floating toggle button; respects system preference and persists via localStorage
See [Configuration](#-configuration) below for detailed parameter reference.
---
## ⚙️ Configuration
### Global Parameters
Access template configuration via **System → Site Templates → MokoOnyx**.
#### Theme Tab
**General Settings:**
- **Theme Enabled**: Enable/disable theme system
- **Theme Control Type**: Switch (Light↔Dark), Radios (Light/Dark/System), or None
- **Default Choice**: System, Light, or Dark
- **Auto Dark Mode**: Automatic dark mode based on time
- **Meta Tags**: Enable color-scheme and theme-color meta tags
- **Bridge Bootstrap ARIA**: Sync theme with Bootstrap's data-bs-theme
**Variables & Palettes:**
- **Light Mode Palette**: Standard, Alternative, or Custom
- **Dark Mode Palette**: Standard, Alternative, or Custom
**Typography:**
- **Font Scheme**: Local (Roboto) or Web fonts (Fira Sans, Roboto+Noto Sans)
**Branding & Icons:**
- **Brand**: Enable/disable site branding
- **Logo File**: Upload custom logo (no default logo included)
- **Site Title**: Custom site title
- **Site Description**: Tagline/description
- **Font Awesome Kit**: Optional FA Pro kit code
**Header & Navigation:**
- **Sticky Header**: Enable fixed header on scroll
- **Back to Top**: Enable floating back-to-top button
**Theme Toggle UI:**
- **FAB Enabled**: Enable floating action button toggle
- **FAB Position**: Bottom-right, Bottom-left, Top-right, or Top-left
#### Advanced Tab
- **Layout**: Static or Fluid container
#### Google Tab
- **Google Tag Manager**: Enable and configure GTM container ID
- **Google Analytics**: Enable and configure GA4 measurement ID
#### Custom Code Tab
- **Custom Head Start**: HTML injected at start of `<head>`
- **Custom Head End**: HTML injected at end of `<head>`
#### Drawers Tab
- **Left Drawer Icon**: Font Awesome icon class (e.g., `fa-solid fa-chevron-right`)
- **Right Drawer Icon**: Font Awesome icon class (e.g., `fa-solid fa-chevron-left`)
### Custom Theme Palettes
MokoOnyx supports custom theme schemes:
1. **Copy template files** from `/templates/` directory:
- `light.custom.css``media/templates/site/mokoonyx/css/theme/light.custom.css`
- `dark.custom.css``media/templates/site/mokoonyx/css/theme/dark.custom.css`
2. **Customize** the CSS variables to match your brand colors
3. **Enable in Joomla**: System → Site Templates → MokoOnyx → Theme tab → Set palette to "Custom"
4. **Save** and view your site with custom colors
**Note:** Custom color files are excluded from version control (`.gitignore`) to prevent fork-specific customizations from being committed.
**Quick Example:**
```css
:root[data-bs-theme="light"] {
--color-primary: #1e40af;
--color-link: #2563eb;
--color-hover: #1d4ed8;
--body-color: #1f2937;
--body-bg: #ffffff;
}
```
**Complete Reference:** See [CSS Variables Documentation](./docs/CSS_VARIABLES.md) for all available variables and detailed usage examples.
### Table of Contents
Enable automatic TOC for articles:
1. Edit an article in Joomla admin
2. Navigate to **Options → Layout**
3. Select **toc-left** or **toc-right**
4. Save the article
The TOC will automatically generate from article headings (H2, H3, etc.) and appear as a sidebar.
---
## 🎨 Theme System
### Dark Mode Features
- **Automatic Detection**: Respects user's system preferences
- **Manual Toggle**: Floating button or radio controls
- **Persistence**: Saves preference in localStorage
- **Smooth Transitions**: Animated theme switching
- **Comprehensive Support**: All components themed for dark mode
### Theme Control Types
1. **Switch**: Simple light/dark toggle button
2. **Radios**: Three options - Light, Dark, System
3. **None**: No visible control (respects system only)
### Meta Tags
When enabled, the template adds:
```html
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#1e3a8a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
```
---
## 🛠 Development
### For Contributors
**New to the project?** See [Quick Start Guide](./docs/QUICK_START.md) for a 5-minute setup.
### Development Resources
- **[Quick Start Guide](./docs/QUICK_START.md)** - Setup and first steps
- **[Joomla Development Guide](./docs/JOOMLA_DEVELOPMENT.md)** - Testing, quality checks, deployment
- **[Workflow Guide](./docs/WORKFLOW_GUIDE.md)** - Git workflow and branching
- **[Contributing Guide](./CONTRIBUTING.md)** - Contribution guidelines
- **[Roadmap](./docs/ROADMAP.md)** - Feature roadmap and planning
### Development Tools
- **Pre-commit Hooks**: Automatic validation before commits
- **PHP CodeSniffer**: Code style validation (Joomla standards)
- **PHPStan**: Static analysis for PHP code
- **Codeception**: Testing framework
### Quick Development Setup
```bash
# Clone repository
git clone https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx.git
cd MokoOnyx
# Install development dependencies (if using Composer)
composer install --dev
# Run code quality checks
make validate # or manual commands
```
### Building Template Package
See [Joomla Development Guide](./docs/JOOMLA_DEVELOPMENT.md) for packaging instructions.
---
## 📚 Documentation
### User Documentation
- **[README](./README.md)** - This file (overview and features)
- **[CHANGELOG](./CHANGELOG.md)** - Version history and changes
- **[Roadmap](./docs/ROADMAP.md)** - Planned features and timeline
### Developer Documentation
- **[Quick Start](./docs/QUICK_START.md)** - 5-minute developer setup
- **[Development Guide](./docs/JOOMLA_DEVELOPMENT.md)** - Comprehensive development guide
- **[Workflow Guide](./docs/WORKFLOW_GUIDE.md)** - Git workflow and processes
- **[CSS Variables Reference](./docs/CSS_VARIABLES.md)** - Complete CSS customization guide
- **[Documentation Index](./docs/README.md)** - All documentation links
### Governance
- **[Contributing](./CONTRIBUTING.md)** - How to contribute
- **[Code of Conduct](./CODE_OF_CONDUCT.md)** - Community standards
- **[Governance](./GOVERNANCE.md)** - Project governance model
- **[Security Policy](./SECURITY.md)** - Security reporting and procedures
---
## 📖 Changelog
See the [CHANGELOG.md](./CHANGELOG.md) for detailed version history.
### Recent Releases
See [Gitea Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases) for all versions.
---
## 💬 Support
### Getting Help
- **Documentation**: Check this README and [docs folder](./docs/)
- **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
Please include:
- Joomla version
- PHP version
- Template version
- Steps to reproduce
- Expected vs actual behavior
- Screenshots (if applicable)
### Security Issues
**Do not** report security vulnerabilities via public issues. See [SECURITY.md](./SECURITY.md) for reporting procedures.
---
## 🤝 Contributing
We welcome contributions! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
### How to Contribute
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes
4. Run quality checks
5. Commit your changes (`git commit -m 'Add amazing feature'`)
6. Push to the branch (`git push origin feature/amazing-feature`)
7. Open a Pull Request
### Development Workflow
See [Workflow Guide](./docs/WORKFLOW_GUIDE.md) for detailed Git workflow.
### Customizations
For template customizations, use Joomla's built-in template settings (System → Site Templates → MokoOnyx → Custom Code tab) for HTML/CSS/JS customizations.
---
## 📦 Included Libraries
MokoOnyx includes the following third-party libraries to provide enhanced functionality:
### Bootstrap TOC
- **Version**: 1.0.1
- **Author**: Aidan Feldman
- **License**: MIT License
- **Source**: [GitHub Repository](https://github.com/afeld/bootstrap-toc)
- **Release**: [v1.0.1 Release](https://github.com/afeld/bootstrap-toc/releases/tag/v1.0.1)
- **Purpose**: Automatically generates a table of contents from article headings with scrollspy support
- **Location**: `src/media/vendor/bootstrap-toc/`
- **Integration**: Registered in `joomla.asset.json` as `vendor.bootstrap-toc` (CSS) and `vendor.bootstrap-toc.js` (JavaScript)
- **Usage**: Activated when using `toc-left` or `toc-right` article layouts
- **Features**:
- Automatic TOC generation from H1-H6 headings
- Hierarchical nested navigation
- Active state highlighting with scrollspy
- Responsive design (collapses on mobile)
- Smooth scrolling to sections
- Automatic unique ID generation for headings
- **Customizations**: CSS adapted to use MokoOnyx CSS variables for theme compatibility
### Font Awesome 7 Free
- **Version**: 7.0 (Free)
- **License**: Font Awesome Free License
- **Source**: [Font Awesome](https://fontawesome.com)
- **Purpose**: Provides 2,000+ vector icons for interface elements
- **Location**: `src/media/vendor/fa7free/`
- **Integration**: Fully integrated into Joomla's asset manager
- **Styles Available**: Solid, Regular, Brands
### Bootstrap 5
- **Version**: 5.x (via Joomla)
- **License**: MIT License
- **Source**: [Bootstrap](https://getbootstrap.com)
- **Purpose**: Provides responsive grid system and utility classes
- **Integration**: Inherited from Joomla's Cassiopeia template, extended with additional helpers
- **Components Used**: Grid, utilities, modal, dropdown, collapse, offcanvas, tooltip, popover, scrollspy
### Integration Method
All third-party libraries are:
- ✅ Properly licensed and attributed
- ✅ Registered in Joomla's Web Asset Manager (`joomla.asset.json`)
- ✅ Loaded on-demand to optimize performance
- ✅ Versioned and documented for maintenance
- ✅ Compatible with Joomla 5.x and 6.x
---
## 📄 License
This project is licensed under the **GNU General Public License v3.0** - see the [LICENSE](./LICENSE) file for details.
### Third-Party Licenses
- **Joomla! CMS**: GPL-2.0-or-later
- **Cassiopeia Template**: GPL-2.0-or-later (Joomla Project)
- **Font Awesome 7 Free**: Font Awesome Free License
- **Bootstrap 5**: MIT License
- **Bootstrap TOC**: MIT License (A. Feld)
All third-party libraries and assets remain the property of their respective authors and are credited in source files.
---
## 🔗 Links
- **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)
---
## 📊 Metadata
- **Maintainer**: Moko Consulting Engineering
- **Author**: Jonathan Miller (@jmiller-moko)
- **Repository**: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
- **License**: GPL-3.0-or-later
- **Classification**: Public Open Source Standards
## 📝 Revision History
| Date | Version | Change Summary | Author |
| ---------- | -------- | ------------------------------------------------------------------------- | ------------------------------- |
| 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 |
---
**Made with ❤️ by [Moko Consulting](https://mokoconsulting.tech)**

View File

@@ -10,9 +10,8 @@
} }
], ],
"require": { "require": {
"ext-zip": "*", "php": ">=8.1",
"mokoconsulting-tech/enterprise": "dev-version/04", "ext-zip": "*"
"php": ">=8.1"
}, },
"require-dev": { "require-dev": {
"mokoconsulting-tech/enterprise": "^4.0" "mokoconsulting-tech/enterprise": "^4.0"

View File

@@ -1,87 +0,0 @@
#!/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

@@ -176,7 +176,7 @@ TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_LABEL="Block Colour System (top-a / top-b / b
TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_DESC="Automatic brand colour palette for modules in <code>top-a</code>, <code>top-b</code>, <code>bottom-a</code>, and <code>bottom-b</code> positions. Colours assigned by <code>:nth-child()</code> order — no classes needed.<br><br><strong>Slot palette</strong><br><code>--block-color-1</code> / <code>--block-text-1</code> — 1st module<br><code>--block-color-2</code> / <code>--block-text-2</code> — 2nd module<br><code>--block-color-3</code> / <code>--block-text-3</code> — 3rd module<br><code>--block-color-4</code> / <code>--block-text-4</code> — 4th module<br><br><strong>Named overrides</strong> (add an ID to the module HTML to bypass slot colour)<br><code>--block-highlight-bg</code> / <code>--block-highlight-text</code> — for <code>#block-highlight</code><br><code>--block-cta-bg</code> / <code>--block-cta-text</code> — for <code>#block-cta</code><br><code>--block-alert-bg</code> / <code>--block-alert-text</code> — for <code>#block-alert</code><br><br><strong>Priority:</strong> Named ID &gt; Slot colour. No <code>!important</code> needed — specificity handles it." TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_DESC="Automatic brand colour palette for modules in <code>top-a</code>, <code>top-b</code>, <code>bottom-a</code>, and <code>bottom-b</code> positions. Colours assigned by <code>:nth-child()</code> order — no classes needed.<br><br><strong>Slot palette</strong><br><code>--block-color-1</code> / <code>--block-text-1</code> — 1st module<br><code>--block-color-2</code> / <code>--block-text-2</code> — 2nd module<br><code>--block-color-3</code> / <code>--block-text-3</code> — 3rd module<br><code>--block-color-4</code> / <code>--block-text-4</code> — 4th module<br><br><strong>Named overrides</strong> (add an ID to the module HTML to bypass slot colour)<br><code>--block-highlight-bg</code> / <code>--block-highlight-text</code> — for <code>#block-highlight</code><br><code>--block-cta-bg</code> / <code>--block-cta-text</code> — for <code>#block-cta</code><br><code>--block-alert-bg</code> / <code>--block-alert-text</code> — for <code>#block-alert</code><br><br><strong>Priority:</strong> Named ID &gt; Slot colour. No <code>!important</code> needed — specificity handles it."
TPL_MOKOONYX_CSS_VARS_HEADER_LABEL="Header Background" TPL_MOKOONYX_CSS_VARS_HEADER_LABEL="Header Background"
TPL_MOKOONYX_CSS_VARS_HEADER_DESC="Controls the background of the topbar/header area.<br><code>--header-background-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_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_CONTAINERS_LABEL="Container Backgrounds" TPL_MOKOONYX_CSS_VARS_CONTAINERS_LABEL="Container Backgrounds"
TPL_MOKOONYX_CSS_VARS_CONTAINERS_DESC="Each layout container has its own background variables. Replace <code>{pos}</code> with: <code>below-topbar</code>, <code>top-a</code>, <code>top-b</code>, <code>sidebar</code>, <code>bottom-a</code>, or <code>bottom-b</code>.<br><br><code>--container-{pos}-bg-image</code> — Background image (default: <code>none</code>)<br><code>--container-{pos}-bg-color</code> — Background colour (default: <code>transparent</code>)<br><code>--container-{pos}-bg-position</code> — Background position<br><code>--container-{pos}-bg-attachment</code> — <code>fixed</code> or <code>scroll</code><br><code>--container-{pos}-bg-repeat</code> — Repeat behaviour<br><code>--container-{pos}-bg-size</code> — e.g. <code>cover</code>, <code>auto</code><br><code>--container-{pos}-border</code> — Border shorthand<br><code>--container-{pos}-border-radius</code> — Border radius<br><br>Also: <code>--container-toc-bg</code> / <code>--container-toc-color</code> for the TOC sidebar." TPL_MOKOONYX_CSS_VARS_CONTAINERS_DESC="Each layout container has its own background variables. Replace <code>{pos}</code> with: <code>below-topbar</code>, <code>top-a</code>, <code>top-b</code>, <code>sidebar</code>, <code>bottom-a</code>, or <code>bottom-b</code>.<br><br><code>--container-{pos}-bg-image</code> — Background image (default: <code>none</code>)<br><code>--container-{pos}-bg-color</code> — Background colour (default: <code>transparent</code>)<br><code>--container-{pos}-bg-position</code> — Background position<br><code>--container-{pos}-bg-attachment</code> — <code>fixed</code> or <code>scroll</code><br><code>--container-{pos}-bg-repeat</code> — Repeat behaviour<br><code>--container-{pos}-bg-size</code> — e.g. <code>cover</code>, <code>auto</code><br><code>--container-{pos}-border</code> — Border shorthand<br><code>--container-{pos}-border-radius</code> — Border radius<br><br>Also: <code>--container-toc-bg</code> / <code>--container-toc-color</code> for the TOC sidebar."

View File

@@ -176,7 +176,7 @@ TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_LABEL="Block Color System (top-a / top-b / bo
TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_DESC="Automatic brand color palette for modules in <code>top-a</code>, <code>top-b</code>, <code>bottom-a</code>, and <code>bottom-b</code> positions. Colors assigned by <code>:nth-child()</code> order — no classes needed.<br><br><strong>Slot palette</strong><br><code>--block-color-1</code> / <code>--block-text-1</code> — 1st module<br><code>--block-color-2</code> / <code>--block-text-2</code> — 2nd module<br><code>--block-color-3</code> / <code>--block-text-3</code> — 3rd module<br><code>--block-color-4</code> / <code>--block-text-4</code> — 4th module<br><br><strong>Named overrides</strong> (add an ID to the module HTML to bypass slot color)<br><code>--block-highlight-bg</code> / <code>--block-highlight-text</code> — for <code>#block-highlight</code><br><code>--block-cta-bg</code> / <code>--block-cta-text</code> — for <code>#block-cta</code><br><code>--block-alert-bg</code> / <code>--block-alert-text</code> — for <code>#block-alert</code><br><br><strong>Priority:</strong> Named ID &gt; Slot color. No <code>!important</code> needed — specificity handles it." TPL_MOKOONYX_CSS_VARS_BLOCK_COLORS_DESC="Automatic brand color palette for modules in <code>top-a</code>, <code>top-b</code>, <code>bottom-a</code>, and <code>bottom-b</code> positions. Colors assigned by <code>:nth-child()</code> order — no classes needed.<br><br><strong>Slot palette</strong><br><code>--block-color-1</code> / <code>--block-text-1</code> — 1st module<br><code>--block-color-2</code> / <code>--block-text-2</code> — 2nd module<br><code>--block-color-3</code> / <code>--block-text-3</code> — 3rd module<br><code>--block-color-4</code> / <code>--block-text-4</code> — 4th module<br><br><strong>Named overrides</strong> (add an ID to the module HTML to bypass slot color)<br><code>--block-highlight-bg</code> / <code>--block-highlight-text</code> — for <code>#block-highlight</code><br><code>--block-cta-bg</code> / <code>--block-cta-text</code> — for <code>#block-cta</code><br><code>--block-alert-bg</code> / <code>--block-alert-text</code> — for <code>#block-alert</code><br><br><strong>Priority:</strong> Named ID &gt; Slot color. No <code>!important</code> needed — specificity handles it."
TPL_MOKOONYX_CSS_VARS_HEADER_LABEL="Header Background" TPL_MOKOONYX_CSS_VARS_HEADER_LABEL="Header Background"
TPL_MOKOONYX_CSS_VARS_HEADER_DESC="Controls the background of the topbar/header area.<br><code>--header-background-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_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_CONTAINERS_LABEL="Container Backgrounds" TPL_MOKOONYX_CSS_VARS_CONTAINERS_LABEL="Container Backgrounds"
TPL_MOKOONYX_CSS_VARS_CONTAINERS_DESC="Each layout container has its own background variables. Replace <code>{pos}</code> with: <code>below-topbar</code>, <code>top-a</code>, <code>top-b</code>, <code>sidebar</code>, <code>bottom-a</code>, or <code>bottom-b</code>.<br><br><code>--container-{pos}-bg-image</code> — Background image (default: <code>none</code>)<br><code>--container-{pos}-bg-color</code> — Background color (default: <code>transparent</code>)<br><code>--container-{pos}-bg-position</code> — Background position<br><code>--container-{pos}-bg-attachment</code> — <code>fixed</code> or <code>scroll</code><br><code>--container-{pos}-bg-repeat</code> — Repeat behavior<br><code>--container-{pos}-bg-size</code> — e.g. <code>cover</code>, <code>auto</code><br><code>--container-{pos}-border</code> — Border shorthand<br><code>--container-{pos}-border-radius</code> — Border radius<br><br>Also: <code>--container-toc-bg</code> / <code>--container-toc-color</code> for the TOC sidebar." TPL_MOKOONYX_CSS_VARS_CONTAINERS_DESC="Each layout container has its own background variables. Replace <code>{pos}</code> with: <code>below-topbar</code>, <code>top-a</code>, <code>top-b</code>, <code>sidebar</code>, <code>bottom-a</code>, or <code>bottom-b</code>.<br><br><code>--container-{pos}-bg-image</code> — Background image (default: <code>none</code>)<br><code>--container-{pos}-bg-color</code> — Background color (default: <code>transparent</code>)<br><code>--container-{pos}-bg-position</code> — Background position<br><code>--container-{pos}-bg-attachment</code> — <code>fixed</code> or <code>scroll</code><br><code>--container-{pos}-bg-repeat</code> — Repeat behavior<br><code>--container-{pos}-bg-size</code> — e.g. <code>cover</code>, <code>auto</code><br><code>--container-{pos}-border</code> — Border shorthand<br><code>--container-{pos}-border-radius</code> — Border radius<br><br>Also: <code>--container-toc-bg</code> / <code>--container-toc-color</code> for the TOC sidebar."

1
src/media/css/editor.min.css vendored Normal file
View File

@@ -0,0 +1 @@
@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

@@ -13927,11 +13927,6 @@ meter {
/* ── HERO CARD BASE ── */ /* ── HERO CARD BASE ── */
.hero { .hero {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
min-height: var(--banner-min-height, 60vh);
max-width: var(--hero-card-max-width, 600px); max-width: var(--hero-card-max-width, 600px);
padding: var(--hero-card-padding-y, 3rem) var(--hero-card-padding-x, 2rem); 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)); background-color: var(--hero-card-bg, var(--hero-primary-bg-color, #0d1e3a));
@@ -13942,12 +13937,6 @@ meter {
border-radius: var(--hero-card-border-radius, .5rem); border-radius: var(--hero-card-border-radius, .5rem);
} }
.hero .card {
margin-left: auto;
margin-right: auto;
text-align: center;
}
/* ── PRIMARY VARIANT (uses default card vars) ── */ /* ── PRIMARY VARIANT (uses default card vars) ── */
.hero#primary { .hero#primary {
background-color: var(--hero-card-bg, #0d1e3a); background-color: var(--hero-card-bg, #0d1e3a);
@@ -14204,11 +14193,10 @@ fieldset>* {
.container-header { .container-header {
z-index: 100; z-index: 100;
background-color: var(--header-background-color, #adadad); background: var(--header-background-image, url('../../../../../../media/templates/site/mokoonyx/images/bg.svg'));
background-image: var(--header-background-image, url('../../../../../../media/templates/site/mokoonyx/images/bg.svg'));
background-size: var(--header-background-size, auto); background-size: var(--header-background-size, auto);
background-repeat: var(--header-background-repeat, repeat);
box-shadow: 0 5px 5px hsla(0, 0%, 0%, 0.03) inset; box-shadow: 0 5px 5px hsla(0, 0%, 0%, 0.03) inset;
background-repeat: var(--header-background-repeat, repeat);
} }
/* Sticky header: override z-index to stay above all content */ /* Sticky header: override z-index to stay above all content */

1
src/media/css/template.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

1
src/media/js/gtm.min.js vendored Normal file
View File

@@ -0,0 +1 @@
(()=>{"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){}})();

1
src/media/js/template.min.js vendored Normal file
View File

@@ -0,0 +1 @@
!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

@@ -93,7 +93,6 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
$this->migrateFromCassiopeia(); $this->migrateFromCassiopeia();
$this->replaceCassiopeiaReferences(); $this->replaceCassiopeiaReferences();
$this->clearFaviconStamp(); $this->clearFaviconStamp();
$this->lockExtension();
} }
return true; return true;
@@ -379,29 +378,6 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
} }
} }
/**
* 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 private function logMessage(string $message, string $priority = 'info'): void
{ {
$priorities = [ $priorities = [

View File

@@ -39,7 +39,7 @@
</server> </server>
</updateservers> </updateservers>
<name>MokoOnyx</name> <name>MokoOnyx</name>
<version>01.00.18</version> <version>01.00.17</version>
<scriptfile>script.php</scriptfile> <scriptfile>script.php</scriptfile>
<creationDate>2026-04-15</creationDate> <creationDate>2026-04-15</creationDate>
<author>Jonathan Miller || Moko Consulting</author> <author>Jonathan Miller || Moko Consulting</author>
@@ -373,6 +373,12 @@
<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_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" /> <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> </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" />
</fieldset>
</fields> </fields>
</config> </config>
</extension> </extension>

View File

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

View File

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

View File

@@ -0,0 +1,836 @@
<!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

@@ -13,13 +13,13 @@
<element>mokoonyx</element> <element>mokoonyx</element>
<type>template</type> <type>template</type>
<client>site</client> <client>site</client>
<version>01.00.24</version> <version>01.00.16</version>
<creationDate>2026-04-23</creationDate> <creationDate>2026-04-22</creationDate>
<infourl title='MokoOnyx Dev'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/development</infourl> <infourl title='MokoOnyx Dev'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/development</infourl>
<downloads> <downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/mokoonyx-01.00.24-dev.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/mokoonyx-01.00.16-dev.zip</downloadurl>
</downloads> </downloads>
<sha256>a2d215c19b3487eeeae62735a618e183c2e665ff75f9031bdb5df4b3c8f299a6</sha256> <sha256>6d0e690995eacd1b2e9193d847abd21369d2eb574406b27515d23f8d5a428c51</sha256>
<tags><tag>development</tag></tags> <tags><tag>development</tag></tags>
<maintainer>Moko Consulting</maintainer> <maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl> <maintainerurl>https://mokoconsulting.tech</maintainerurl>
@@ -81,6 +81,7 @@
<infourl title='MokoOnyx RC'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/release-candidate</infourl> <infourl title='MokoOnyx RC'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/release-candidate</infourl>
<downloads> <downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/release-candidate/mokoonyx-01.00.07-rc.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/release-candidate/mokoonyx-01.00.07-rc.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoOnyx/releases/download/release-candidate/mokoonyx-01.00.07-rc.zip</downloadurl>
</downloads> </downloads>
<sha256></sha256> <sha256></sha256>
<tags><tag>rc</tag></tags> <tags><tag>rc</tag></tags>
@@ -97,13 +98,14 @@
<element>mokoonyx</element> <element>mokoonyx</element>
<type>template</type> <type>template</type>
<client>site</client> <client>site</client>
<version>01.00.25</version> <version>01.00.17</version>
<creationDate>2026-04-23</creationDate> <creationDate>2026-04-22</creationDate>
<infourl title='MokoOnyx'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/stable</infourl> <infourl title='MokoOnyx'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/stable</infourl>
<downloads> <downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/stable/mokoonyx-01.00.25.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/stable/mokoonyx-01.00.17.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoOnyx/releases/download/stable/mokoonyx-01.00.17.zip</downloadurl>
</downloads> </downloads>
<sha256>d111a027bdd5f1eae1fe973933fa4043be2740d62fea3527fb856275f8fb3be4</sha256> <sha256>f4811005774b99e54513cd2cae052b815ef085e2a04f3a91cd5c0bc9cff7ffc2</sha256>
<tags><tag>stable</tag></tags> <tags><tag>stable</tag></tags>
<maintainer>Moko Consulting</maintainer> <maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl> <maintainerurl>https://mokoconsulting.tech</maintainerurl>