Compare commits
183 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 405cd0eb25 | |||
| 937f30ac03 | |||
| 9b48be321b | |||
| 3b5473332e | |||
| 998055364f | |||
| 6c9ce3a1c1 | |||
| ff8a815393 | |||
| 8fa43e7e3e | |||
| 298a72351d | |||
| 5686f072c6 | |||
| ee9b662bb2 | |||
| 40c36f7f7c | |||
| 6645fdf041 | |||
| da66a969d2 | |||
| cf77ec47df | |||
| cd31bb8a0e | |||
| 1246bc6369 | |||
| 632a0a0a2f | |||
| a73f15e100 | |||
| 3392f0c041 | |||
| 0fd0248cb6 | |||
| 26d52bb00d | |||
| b5b6dcde16 | |||
| 041d8ebe28 | |||
| 35a3c50ae8 | |||
| 74e4131ea8 | |||
| a607be2630 | |||
| bf8b6ef13f | |||
| 0fada4ae3e | |||
| 6eb250c4af | |||
| 83df766c8e | |||
| 7dc7d2e0aa | |||
| 3da0cd600e | |||
| aff82d599f | |||
| 7ec9b847fc | |||
| 6c6f813c26 | |||
| cebbb33d87 | |||
| 8b7ea2e2c2 | |||
| ce9b5f0e5c | |||
| 59875466f4 | |||
| 32cb7cecd6 | |||
| f573ac0a77 | |||
| 24e42d9132 | |||
| 96eda294fd | |||
| d957022fc1 | |||
| 82d5beb0f0 | |||
| bf202a4a50 | |||
| 9968c81660 | |||
| 6802b256d0 | |||
| 7ebd2e6385 | |||
| d0ea5e43c0 | |||
| e0ef643c04 | |||
| 7b9ea92faa | |||
| 9f628201ad | |||
| 86f341bd72 | |||
| 4cdf7234d9 | |||
| e19766cf00 | |||
| 368d5eb2ed | |||
| 6c9538e36c | |||
| 08ec94953a | |||
| 9b30244361 | |||
| 121219a810 | |||
| 6cb8751f89 | |||
| 3b118d7b49 | |||
| fccd80da3c | |||
| 58fb87bb05 | |||
| 9680004f78 | |||
| 86faa5086f | |||
| 696c5a45c4 | |||
| cbdbdcd553 | |||
| a5bab02fd1 | |||
| c9a2cfa15c | |||
| af76a8f2cb | |||
| 1001df963e | |||
| ed7d356e99 | |||
| 396b60902a | |||
| c7d55e7fdd | |||
| ad7fc53fb9 | |||
| ad6a7e90e9 | |||
| 041111b8ab | |||
| b65dd95dc4 | |||
| 8428f29058 | |||
| 9e6cebb9aa | |||
| 595ee55499 | |||
| 6e05b6aea0 | |||
| 35bc550e0c | |||
| c6b52100de | |||
| c9470a45fe | |||
| 7962b370fa | |||
| 0e4b0d55cb | |||
| 2f7cbb57f9 | |||
| 006b47bbdf | |||
| 0cc2fea1bc | |||
| 5a9e8d86a1 | |||
| 0cdd186eae | |||
| 5e77328b66 | |||
| ac05b97262 | |||
| b1391c6d03 | |||
| ec75322bed | |||
| 6a6a8d7c0c | |||
| 3d059a0ad8 | |||
| 28b4916d94 | |||
| 2addc3bdb4 | |||
| b573c6e762 | |||
| 0a1e788f00 | |||
| b051e49320 | |||
| 4b0c024299 | |||
| 951221bcc0 | |||
| 3d357d1bd3 | |||
| 9062a3141e | |||
| 41d8ce7a2a | |||
| 0e9703f8b0 | |||
| 8abadc1709 | |||
| 706f3137b7 | |||
| d28877fc0c | |||
| 43a7432f08 | |||
| a12c6ece42 | |||
| adba742cfe | |||
| 6929868dd7 | |||
| 99ab99dfe2 | |||
| 0e4e329770 | |||
| 35f79bc53e | |||
| 50762b3e20 | |||
| 0e420718cf | |||
| 9f88c265bc | |||
| 9ace43be1a | |||
| 0b47958b28 | |||
| ad5bb9d46a | |||
| 1cfc8f8669 | |||
| 28da101822 | |||
| 0dffd277e0 | |||
| be08e707f1 | |||
| e265edb4ba | |||
| 98fddcbedb | |||
| 2886ebdf54 | |||
| 36c15a3d86 | |||
| 4ceb9efbf0 | |||
| 6ce2cdb2cb | |||
| 6c61c13db6 | |||
| 97466819c5 | |||
| 61adcb0bc9 | |||
| 6d1aaa952b | |||
| 2aeb4d5ecc | |||
| b1fb0d2294 | |||
| da91c6c820 | |||
| f08caa6125 | |||
| 5ab9729694 | |||
| 04ca05ccc3 | |||
| eac32646cb | |||
| 1859dd728c | |||
| 7b02af2b76 | |||
| c4a7925f7f | |||
| d357bf958a | |||
| 2242a61197 | |||
| 74c433a98b | |||
| 3f9e700876 | |||
| f8aae4c639 | |||
| a16949fd4d | |||
| faead32d07 | |||
| f3897495ad | |||
| 64cbc5674e | |||
| e7a83e9232 | |||
| eaa2c1808c | |||
| 17ccd019de | |||
| 3a543aa7c5 | |||
| 00194a2fdc | |||
| 14eeb80569 | |||
| d757e009e3 | |||
| 16a7090f29 | |||
| 33cb75af26 | |||
| e8393b0322 | |||
| 3a30f1a088 | |||
| 872b5329a3 | |||
| 8ae203cca0 | |||
| 7ca7c6713c | |||
| 18a5934106 | |||
| 2d25ae2359 | |||
| 94680bbd3f | |||
| d4e1c96224 | |||
| 1170fb21bf | |||
| a2d494544b | |||
| c42a23d739 | |||
| dda2d4b20e |
+1
-1
@@ -113,7 +113,7 @@ releases/
|
|||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
out/
|
out/
|
||||||
site/
|
/site/
|
||||||
*.map
|
*.map
|
||||||
*.css.map
|
*.css.map
|
||||||
*.js.map
|
*.js.map
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# MokoOnyx
|
||||||
|
|
||||||
|
Joomla site template — successor to MokoCassiopeia. Base template for all WaaS client deployments.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Element** | `tpl_mokoonyx` |
|
||||||
|
| **Type** | Joomla site template |
|
||||||
|
| **Language** | PHP 8.1+ / CSS / JS |
|
||||||
|
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||||
|
| **Wiki** | [MokoOnyx Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/wiki) |
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build # Build template ZIP
|
||||||
|
make lint # Run linters
|
||||||
|
make validate # Validate structure
|
||||||
|
make release # Full release pipeline
|
||||||
|
make minify # Minify CSS/JS assets
|
||||||
|
make clean # Clean build artifacts
|
||||||
|
composer install # Install PHP dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Joomla **site template** — the base layer that client theme packages override:
|
||||||
|
|
||||||
|
- `src/templateDetails.xml` — template manifest
|
||||||
|
- `src/index.php` — main template entry point
|
||||||
|
- `src/error.php` — error page
|
||||||
|
- `src/offline.php` — maintenance page
|
||||||
|
- `src/component.php` — print/component-only layout
|
||||||
|
- `src/html/` — template overrides for core components
|
||||||
|
- `src/media/css/` — base stylesheets
|
||||||
|
- `src/media/js/` — base scripts
|
||||||
|
- `src/media/images/` — template images
|
||||||
|
- `src/language/` — translations
|
||||||
|
|
||||||
|
### Client Theme Packages
|
||||||
|
|
||||||
|
Client repos (`client-clarksvillefurs`, `client-optainfunding`, etc.) install `type="file"` packages that overlay client-specific CSS, images, and JS into the MokoOnyx media directory. MokoOnyx provides the structure; client themes customize the appearance.
|
||||||
|
|
||||||
|
### Minification
|
||||||
|
|
||||||
|
`MokoMinifyHelper` handles runtime CSS/JS minification in Joomla. Build-time minification via `make minify`. Never commit `*.min.css`/`*.min.js` — they're generated.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
||||||
|
- **Attribution**: `Authored-by: Moko Consulting`
|
||||||
|
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||||
|
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper)
|
||||||
|
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
||||||
|
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
|
||||||
|
- PHP 8.1+ minimum
|
||||||
|
- SPDX license headers on all PHP files
|
||||||
|
- `defined('_JEXEC') or die;` on all PHP files
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# DISABLED - auto-release handles dev recreation from main
|
# DISABLED - auto-release handles dev recreation
|
||||||
name: Cascade (DISABLED)
|
name: Cascade (DISABLED)
|
||||||
on: workflow_dispatch
|
on: workflow_dispatch
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<display-name>Template - MokoOnyx</display-name>
|
<display-name>Template - MokoOnyx</display-name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>MokoOnyx - Joomla site template (successor to MokoCassiopeia)</description>
|
<description>MokoOnyx - Joomla site template (successor to MokoCassiopeia)</description>
|
||||||
<version>02.15.02</version>
|
<version>02.21.05</version>
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
<governance>
|
<governance>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
# | Platform-specific: |
|
# | Platform-specific: |
|
||||||
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
# | joomla: XML manifest, type-prefixed packages |
|
||||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
# | generic: README-only, no update stream |
|
# | generic: README-only, no update stream |
|
||||||
# | |
|
# | |
|
||||||
@@ -71,20 +71,25 @@ jobs:
|
|||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||||
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
|
echo Using pre-installed /opt/moko-platform
|
||||||
|
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo Falling back to fresh clone
|
||||||
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
|
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
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||||
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api
|
|
||||||
composer install --no-dev --no-interaction --quiet
|
|
||||||
|
|
||||||
- name: Rename branch to rc
|
- name: Rename branch to rc
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/branch_rename.php \
|
php ${MOKO_CLI}/branch_rename.php \
|
||||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
@@ -100,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish RC release
|
- name: Publish RC release
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
@@ -108,7 +113,7 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
|
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
release:
|
release:
|
||||||
@@ -131,31 +136,80 @@ jobs:
|
|||||||
git config --local user.name "gitea-actions[bot]"
|
git config --local user.name "gitea-actions[bot]"
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
- name: Check for merge conflict markers
|
||||||
|
run: |
|
||||||
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
|
if [ -n "$CONFLICTS" ]; then
|
||||||
|
echo "::error::Merge conflict markers found — aborting release"
|
||||||
|
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "No conflict markers found"
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
run: |
|
run: |
|
||||||
# Ensure PHP + Composer are available
|
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||||
if ! command -v composer &> /dev/null; then
|
echo Using pre-installed /opt/moko-platform
|
||||||
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
|
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo Falling back to fresh clone
|
||||||
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
|
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
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||||
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api
|
|
||||||
composer install --no-dev --no-interaction --quiet
|
|
||||||
|
|
||||||
|
|
||||||
- name: "Publish stable release"
|
- name: "Publish stable release"
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
--path . --stability stable --bump minor --branch main \
|
--path . --stability stable --bump minor --branch main \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
- name: Update release notes from CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Extract [Unreleased] section from changelog
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Stable release"
|
||||||
|
else
|
||||||
|
NOTES="Stable release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update release body via API
|
||||||
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import json, urllib.request
|
||||||
|
body = open('/dev/stdin').read()
|
||||||
|
payload = json.dumps({'body': body}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
|
data=payload, method='PATCH',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
" <<< "$NOTES"
|
||||||
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
|
fi
|
||||||
|
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
if: >-
|
if: >-
|
||||||
@@ -167,7 +221,7 @@ jobs:
|
|||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php /tmp/moko-platform-api/cli/release_mirror.php \
|
php ${MOKO_CLI}/release_mirror.php \
|
||||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
@@ -241,7 +295,7 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
php ${MOKO_CLI}/version_reset_dev.php \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
--branch dev --path . 2>&1 || true
|
--branch dev --path . 2>&1 || true
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# DISABLED - auto-release handles dev recreation from main
|
# DISABLED — auto-release Step 11 recreates dev from main after every release.
|
||||||
name: Cascade (DISABLED)
|
# Cascade-dev is redundant and causes version conflicts when both main and dev
|
||||||
|
# have different version numbers in templateDetails.xml / manifest.xml.
|
||||||
|
name: "Cascade Main → Dev (DISABLED)"
|
||||||
on: workflow_dispatch
|
on: workflow_dispatch
|
||||||
jobs:
|
jobs:
|
||||||
noop:
|
noop:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: echo disabled
|
- run: echo "Cascade disabled — auto-release handles dev recreation"
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.CI
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
|
# PATH: /.gitea/workflows/ci-generic.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
||||||
|
|
||||||
|
name: "Generic: Project CI"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
- dev/**
|
||||||
|
- rc/**
|
||||||
|
- version/**
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
- dev/**
|
||||||
|
- rc/**
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ── Lint & Validate ───────────────────────────────────────────────────
|
||||||
|
lint:
|
||||||
|
name: Lint & Validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Detect toolchain
|
||||||
|
id: detect
|
||||||
|
run: |
|
||||||
|
HAS_PHP=false
|
||||||
|
HAS_NODE=false
|
||||||
|
[ -f "composer.json" ] && HAS_PHP=true
|
||||||
|
[ -f "package.json" ] && HAS_NODE=true
|
||||||
|
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
php -v
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install PHP dependencies
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install Node.js dependencies
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "package.json" ]; then
|
||||||
|
npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: PHP syntax check
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||||
|
echo "::error file=${file}::PHP syntax error"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
|
||||||
|
|
||||||
|
echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$ERRORS" -eq 0 ]; then
|
||||||
|
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: TypeScript/JavaScript lint
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "node_modules/.bin/eslint" ]; then
|
||||||
|
npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
|
||||||
|
echo "## ESLint" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
|
||||||
|
echo "::warning::ESLint config found but eslint not installed"
|
||||||
|
else
|
||||||
|
echo "No ESLint configured — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: TypeScript compile check
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
|
||||||
|
npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
|
||||||
|
echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: PHPStan static analysis
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
|
||||||
|
vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Tests ─────────────────────────────────────────────────────────────
|
||||||
|
test:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Detect toolchain
|
||||||
|
id: detect
|
||||||
|
run: |
|
||||||
|
HAS_PHP=false
|
||||||
|
HAS_NODE=false
|
||||||
|
[ -f "composer.json" ] && HAS_PHP=true
|
||||||
|
[ -f "package.json" ] && HAS_NODE=true
|
||||||
|
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
[ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
||||||
|
[ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
|
||||||
|
|
||||||
|
- name: Run PHP tests
|
||||||
|
if: steps.detect.outputs.has_php == 'true'
|
||||||
|
run: |
|
||||||
|
if [ -f "vendor/bin/phpunit" ]; then
|
||||||
|
vendor/bin/phpunit --testdox 2>&1
|
||||||
|
echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
|
||||||
|
echo "::warning::PHPUnit config found but phpunit not installed"
|
||||||
|
else
|
||||||
|
echo "No PHPUnit configured — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run Node.js tests
|
||||||
|
if: steps.detect.outputs.has_node == 'true'
|
||||||
|
run: |
|
||||||
|
if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
|
||||||
|
npm test 2>&1
|
||||||
|
echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "No test script in package.json — skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build check
|
||||||
|
run: |
|
||||||
|
if [ -f "Makefile" ]; then
|
||||||
|
make build 2>&1 || echo "::warning::Build failed or not configured"
|
||||||
|
elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
|
||||||
|
npm run build 2>&1 || echo "::warning::Build failed"
|
||||||
|
fi
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
|
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
|
||||||
# VERSION: 04.06.00
|
# VERSION: 04.06.00
|
||||||
# BRIEF: CI workflow for Joomla extensions -- lint, validate, test
|
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
|
||||||
|
|
||||||
name: "Joomla: Extension CI"
|
name: "Joomla: Extension CI"
|
||||||
|
|
||||||
@@ -35,25 +35,32 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
run: |
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
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
|
||||||
php -v && composer --version
|
php -v && composer --version
|
||||||
|
|
||||||
- name: Clone MokoStandards
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch main --quiet \
|
if [ -d "/tmp/moko-platform" ] || [ -d "/opt/moko-platform" ]; then
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
echo "moko-platform already available on runner — skipping clone"
|
||||||
/tmp/mokostandards-api
|
else
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform 2>/dev/null || echo "moko-platform clone skipped — continuing without it"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install \
|
composer install \
|
||||||
@@ -61,7 +68,7 @@ jobs:
|
|||||||
--prefer-dist \
|
--prefer-dist \
|
||||||
--optimize-autoloader
|
--optimize-autoloader
|
||||||
else
|
else
|
||||||
echo "No composer.json found -- skipping dependency install"
|
echo "No composer.json found — skipping dependency install"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: PHP syntax check
|
- name: PHP syntax check
|
||||||
@@ -124,8 +131,8 @@ jobs:
|
|||||||
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
|
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check required tags: name, version, author, namespace (Joomla 5+)
|
# Check required tags: name, version, author
|
||||||
for TAG in name version author namespace; do
|
for TAG in name version author; do
|
||||||
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
|
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
|
||||||
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
|
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
|
||||||
ERRORS=$((ERRORS + 1))
|
ERRORS=$((ERRORS + 1))
|
||||||
@@ -133,6 +140,19 @@ jobs:
|
|||||||
echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
|
echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Namespace is required for components/plugins but not packages
|
||||||
|
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
|
||||||
|
if [ "$EXT_TYPE" != "package" ]; then
|
||||||
|
if ! grep -q "<namespace" "$MANIFEST" 2>/dev/null; then
|
||||||
|
echo "Missing required tag: \`<namespace>\` (required for Joomla 5+ ${EXT_TYPE} extensions)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Found required tag: \`<namespace>\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Package extension — \`<namespace>\` not required." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${ERRORS}" -gt 0 ]; then
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
@@ -161,7 +181,7 @@ jobs:
|
|||||||
# Extract language file references from manifest
|
# Extract language file references from manifest
|
||||||
LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
||||||
if [ -z "$LANG_FILES" ]; then
|
if [ -z "$LANG_FILES" ]; then
|
||||||
echo "No language file references found in manifest -- skipping." >> $GITHUB_STEP_SUMMARY
|
echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
else
|
else
|
||||||
while IFS= read -r LANG_FILE; do
|
while IFS= read -r LANG_FILE; do
|
||||||
LANG_FILE=$(echo "$LANG_FILE" | xargs)
|
LANG_FILE=$(echo "$LANG_FILE" | xargs)
|
||||||
@@ -185,7 +205,7 @@ jobs:
|
|||||||
done <<< "$LANG_FILES"
|
done <<< "$LANG_FILES"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "No manifest found -- skipping language check." >> $GITHUB_STEP_SUMMARY
|
echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${ERRORS}" -gt 0 ]; then
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
@@ -216,7 +236,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [ "${CHECKED}" -eq 0 ]; then
|
if [ "${CHECKED}" -eq 0 ]; then
|
||||||
echo "No src/ or htdocs/ directories found -- skipping." >> $GITHUB_STEP_SUMMARY
|
echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
elif [ "${MISSING}" -gt 0 ]; then
|
elif [ "${MISSING}" -gt 0 ]; then
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
|
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -232,7 +252,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Validate release readiness
|
- name: Validate release readiness
|
||||||
run: |
|
run: |
|
||||||
@@ -338,15 +358,19 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup PHP ${{ matrix.php }}
|
- name: Setup PHP ${{ matrix.php }}
|
||||||
run: |
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
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
|
||||||
php -v && composer --version
|
php -v && composer --version
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install \
|
composer install \
|
||||||
@@ -354,7 +378,7 @@ jobs:
|
|||||||
--prefer-dist \
|
--prefer-dist \
|
||||||
--optimize-autoloader
|
--optimize-autoloader
|
||||||
else
|
else
|
||||||
echo "No composer.json found -- skipping dependency install"
|
echo "No composer.json found — skipping dependency install"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
@@ -366,14 +390,14 @@ jobs:
|
|||||||
if [ $EXIT -eq 0 ]; then
|
if [ $EXIT -eq 0 ]; then
|
||||||
echo "All tests passed." >> $GITHUB_STEP_SUMMARY
|
echo "All tests passed." >> $GITHUB_STEP_SUMMARY
|
||||||
else
|
else
|
||||||
echo "Test failures detected -- see log." >> $GITHUB_STEP_SUMMARY
|
echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY
|
||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY
|
cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY
|
||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
exit $EXIT
|
exit $EXIT
|
||||||
else
|
else
|
||||||
echo "No phpunit.xml found -- skipping tests." >> $GITHUB_STEP_SUMMARY
|
echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
static-analysis:
|
static-analysis:
|
||||||
@@ -384,14 +408,19 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
run: php -v && composer --version
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
|
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
|
||||||
|
php -v && composer --version
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install --no-interaction --prefer-dist --optimize-autoloader
|
composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||||
@@ -422,7 +451,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
if [ -z "$SRC_DIR" ]; then
|
if [ -z "$SRC_DIR" ]; then
|
||||||
echo "No source directory found (src/, htdocs/, lib/) -- skipping." >> $GITHUB_STEP_SUMMARY
|
echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -432,7 +461,7 @@ jobs:
|
|||||||
echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
|
echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
|
||||||
else
|
else
|
||||||
ARGS="$ARGS --level=3"
|
ARGS="$ARGS --level=3"
|
||||||
echo "No phpstan.neon found -- using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
|
echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
$PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
|
$PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
|
||||||
@@ -458,10 +487,14 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Trigger pre-release build
|
- name: Trigger pre-release build
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
BRANCH: ${{ github.head_ref }}
|
BRANCH: ${{ github.head_ref }}
|
||||||
run: |
|
run: |
|
||||||
curl -s -X POST "${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
curl -s -X POST \
|
||||||
|
"${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Maintenance
|
# INGROUP: MokoStandards.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.gitea/workflows/cleanup.yml
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
@@ -33,17 +33,17 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
|
||||||
- name: Delete merged branches
|
- name: Delete merged branches
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Merged Branch Cleanup ==="
|
echo "=== Merged Branch Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
# List branches via API
|
# List branches via API
|
||||||
BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
# Check if branch is merged into main
|
# Check if branch is merged into main
|
||||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||||
echo " Deleting merged branch: ${BRANCH}"
|
echo " Deleting merged branch: ${BRANCH}"
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
fi
|
fi
|
||||||
@@ -66,20 +66,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Clean old workflow runs
|
- name: Clean old workflow runs
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Workflow Run Cleanup ==="
|
echo "=== Workflow Run Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
# Get old completed runs
|
# Get old completed runs
|
||||||
RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/actions/runs?status=completed&limit=50" | \
|
"${API}/actions/runs?status=completed&limit=50" | \
|
||||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
for RUN_ID in $RUNS; do
|
for RUN_ID in $RUNS; do
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Deploy
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
|
||||||
|
# VERSION: 04.07.00
|
||||||
|
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
|
||||||
|
|
||||||
|
name: "Universal: Deploy to Dev (Manual)"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
clear_remote:
|
||||||
|
description: 'Delete all remote files before uploading'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: SFTP Deploy to Dev
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
run: |
|
||||||
|
php -v && composer --version
|
||||||
|
|
||||||
|
- name: Setup MokoStandards tools
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
|
run: |
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
|
/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
|
||||||
|
|
||||||
|
- name: Check FTP configuration
|
||||||
|
id: check
|
||||||
|
env:
|
||||||
|
HOST: ${{ vars.DEV_FTP_HOST }}
|
||||||
|
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
|
||||||
|
PORT: ${{ vars.DEV_FTP_PORT }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
|
||||||
|
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "host=$HOST" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
REMOTE="${PATH_VAR%/}"
|
||||||
|
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
[ -z "$PORT" ] && PORT="22"
|
||||||
|
echo "port=$PORT" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Deploy via SFTP
|
||||||
|
if: steps.check.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||||
|
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||||
|
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||||
|
run: |
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
|
||||||
|
|
||||||
|
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||||
|
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
|
||||||
|
> /tmp/sftp-config.json
|
||||||
|
|
||||||
|
if [ -n "$SFTP_KEY" ]; then
|
||||||
|
echo "$SFTP_KEY" > /tmp/deploy_key
|
||||||
|
chmod 600 /tmp/deploy_key
|
||||||
|
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||||
|
else
|
||||||
|
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
||||||
|
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
|
||||||
|
|
||||||
|
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
|
||||||
|
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
||||||
|
else
|
||||||
|
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
|
||||||
|
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Security
|
# INGROUP: MokoStandards.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
# PATH: /templates/workflows/gitleaks.yml.template
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
|
|||||||
@@ -5,17 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Automation
|
# INGROUP: moko-platform.Automation
|
||||||
<<<<<<< HEAD
|
# VERSION: 02.21.05
|
||||||
<<<<<<< HEAD
|
|
||||||
# VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
# VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
# VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
# VERSION: 02.15.02
|
|
||||||
>>>>>>> origin/main
|
|
||||||
>>>>>>> origin/main
|
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Notifications
|
# INGROUP: MokoStandards.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.gitea/workflows/notify.yml
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# INGROUP: moko-platform.CI
|
# INGROUP: moko-platform.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
# VERSION: 05.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: PR gate — branch policy + code validation before merge
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
|
|
||||||
name: "Universal: PR Check"
|
name: "Universal: PR Check"
|
||||||
@@ -105,6 +105,19 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check for merge conflict markers
|
||||||
|
run: |
|
||||||
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
|
if [ -n "$CONFLICTS" ]; then
|
||||||
|
echo "::error::Merge conflict markers found in source files"
|
||||||
|
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "No conflict markers found"
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
run: |
|
run: |
|
||||||
@@ -134,6 +147,98 @@ jobs:
|
|||||||
echo "PHP lint: ${ERRORS} error(s)"
|
echo "PHP lint: ${ERRORS} error(s)"
|
||||||
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
||||||
|
|
||||||
|
- name: Joomla JEXEC guard check
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
# Skip vendor, node_modules, and index.html stub files
|
||||||
|
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
|
||||||
|
# Check first 10 lines for JEXEC or JPATH guard
|
||||||
|
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
|
||||||
|
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
|
||||||
|
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "JEXEC guard: OK"
|
||||||
|
|
||||||
|
- name: Joomla directory listing protection
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
MISSING=0
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
|
while IFS= read -r dir; do
|
||||||
|
if [ ! -f "${dir}/index.html" ]; then
|
||||||
|
echo "::warning::Missing index.html in ${dir} (directory listing protection)"
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
|
||||||
|
if [ "$MISSING" -gt 0 ]; then
|
||||||
|
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "Directory protection: ${MISSING} missing (advisory)"
|
||||||
|
|
||||||
|
- name: Joomla script file and asset checks
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
[ -z "$MANIFEST" ] && exit 0
|
||||||
|
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||||
|
|
||||||
|
# Check scriptfile exists if declared
|
||||||
|
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
|
||||||
|
if [ -n "$SCRIPTFILE" ]; then
|
||||||
|
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
|
||||||
|
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Require joomla.asset.json and validate it
|
||||||
|
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$ASSET_JSON" ]; then
|
||||||
|
echo "::error::joomla.asset.json not found — Joomla asset system is required"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
|
||||||
|
echo "::error::joomla.asset.json is not valid JSON"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
echo "joomla.asset.json: valid"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate all XML files in src/ are well-formed
|
||||||
|
XML_ERRORS=0
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
while IFS= read -r -d '' xmlfile; do
|
||||||
|
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
|
||||||
|
XML_ERRORS=$((XML_ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
|
||||||
|
fi
|
||||||
|
if [ "$XML_ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::${XML_ERRORS} XML file(s) are malformed"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "XML well-formedness: OK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$ERRORS" -gt 0 ] && exit 1
|
||||||
|
echo "Joomla asset checks: OK"
|
||||||
|
|
||||||
- name: Validate platform manifest
|
- name: Validate platform manifest
|
||||||
run: |
|
run: |
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
@@ -151,6 +256,13 @@ jobs:
|
|||||||
for ELEMENT in name version description; do
|
for ELEMENT in name version description; do
|
||||||
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
||||||
done
|
done
|
||||||
|
# Block legacy raw/branch update server URLs on MokoGitea
|
||||||
|
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
|
||||||
|
if [ -n "$RAW_URLS" ]; then
|
||||||
|
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
|
||||||
|
echo "$RAW_URLS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "Joomla manifest valid"
|
echo "Joomla manifest valid"
|
||||||
;;
|
;;
|
||||||
dolibarr)
|
dolibarr)
|
||||||
@@ -183,6 +295,138 @@ jobs:
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
- name: Validate Joomla language files
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
# Require both en-GB and en-US language directories
|
||||||
|
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$LANG_ROOT" ]; then
|
||||||
|
echo "No language/ directory found — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$LANG_ROOT/en-GB" ]; then
|
||||||
|
echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
if [ ! -d "$LANG_ROOT/en-US" ]; then
|
||||||
|
echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that en-GB and en-US have matching .ini files
|
||||||
|
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
|
||||||
|
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
|
||||||
|
[ ! -f "$GB_INI" ] && continue
|
||||||
|
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
|
||||||
|
if [ ! -f "$US_INI" ]; then
|
||||||
|
echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
|
||||||
|
[ ! -f "$US_INI" ] && continue
|
||||||
|
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
|
||||||
|
if [ ! -f "$GB_INI" ]; then
|
||||||
|
echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find all .ini language files
|
||||||
|
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
|
||||||
|
if [ -z "$INI_FILES" ]; then
|
||||||
|
echo "No .ini language files found"
|
||||||
|
[ "$ERRORS" -gt 0 ] && exit 1
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
|
||||||
|
|
||||||
|
for FILE in $INI_FILES; do
|
||||||
|
FNAME=$(basename "$FILE")
|
||||||
|
LINENUM=0
|
||||||
|
SEEN_KEYS=""
|
||||||
|
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
LINENUM=$((LINENUM + 1))
|
||||||
|
|
||||||
|
# Skip empty lines and comments
|
||||||
|
[ -z "$line" ] && continue
|
||||||
|
echo "$line" | grep -qE '^\s*;' && continue
|
||||||
|
echo "$line" | grep -qE '^\s*$' && continue
|
||||||
|
|
||||||
|
# Must match KEY="VALUE" format
|
||||||
|
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
|
||||||
|
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract key and check for duplicates
|
||||||
|
KEY=$(echo "$line" | sed 's/=.*//')
|
||||||
|
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
|
||||||
|
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
SEEN_KEYS="${SEEN_KEYS}
|
||||||
|
${KEY}"
|
||||||
|
done < "$FILE"
|
||||||
|
|
||||||
|
echo " ${FILE}: checked ${LINENUM} lines"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Cross-check en-GB vs en-US key consistency
|
||||||
|
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
|
||||||
|
for GB_FILE in "$GB_DIR"/*.ini; do
|
||||||
|
[ ! -f "$GB_FILE" ] && continue
|
||||||
|
FNAME=$(basename "$GB_FILE")
|
||||||
|
US_FILE="$US_DIR/$FNAME"
|
||||||
|
[ ! -f "$US_FILE" ] && continue
|
||||||
|
|
||||||
|
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
|
||||||
|
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
|
||||||
|
|
||||||
|
# Keys in en-GB but not en-US
|
||||||
|
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||||
|
if [ -n "$MISSING_US" ]; then
|
||||||
|
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
|
||||||
|
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keys in en-US but not en-GB
|
||||||
|
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||||
|
if [ -n "$MISSING_GB" ]; then
|
||||||
|
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
|
||||||
|
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Language File Validation"
|
||||||
|
echo "| Metric | Count |"
|
||||||
|
echo "|---|---|"
|
||||||
|
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
|
||||||
|
echo "| Errors | ${ERRORS} |"
|
||||||
|
echo "| Warnings | ${WARNINGS} |"
|
||||||
|
} >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::Language validation failed with ${ERRORS} error(s)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Language files: OK (${WARNINGS} warning(s))"
|
||||||
|
|
||||||
- name: Check changelog has unreleased entry
|
- name: Check changelog has unreleased entry
|
||||||
run: |
|
run: |
|
||||||
if [ ! -f "CHANGELOG.md" ]; then
|
if [ ! -f "CHANGELOG.md" ]; then
|
||||||
@@ -234,3 +478,31 @@ jobs:
|
|||||||
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Issue Reporter ──────────────────────────────────────────────────────
|
||||||
|
report-issues:
|
||||||
|
name: Report Issues
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [branch-policy, validate]
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
needs.validate.result == 'failure'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: automation/ci-issue-reporter.sh
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: "File issue for PR validation failure"
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
chmod +x automation/ci-issue-reporter.sh
|
||||||
|
./automation/ci-issue-reporter.sh \
|
||||||
|
--gate "PR Validation" \
|
||||||
|
--workflow "PR Check" \
|
||||||
|
--severity error \
|
||||||
|
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
||||||
|
|||||||
@@ -8,15 +8,22 @@
|
|||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
push:
|
||||||
types: [closed]
|
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
|
- 'fix/**'
|
||||||
|
- 'patch/**'
|
||||||
|
- 'hotfix/**'
|
||||||
|
- 'bugfix/**'
|
||||||
|
- 'chore/**'
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- rc
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
stability:
|
stability:
|
||||||
@@ -39,11 +46,11 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
github.event_name == 'push'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -51,32 +58,50 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
||||||
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
|
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||||
|
echo Using pre-installed /opt/moko-platform
|
||||||
|
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo Falling back to fresh clone
|
||||||
|
if ! command -v composer > /dev/null 2>&1; then
|
||||||
|
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
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||||
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
|
||||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
run: |
|
run: |
|
||||||
|
# Auto-detect and update platform if not set in manifest
|
||||||
|
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
- name: Resolve metadata and bump version
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
STABILITY="${{ inputs.stability || 'development' }}"
|
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||||
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
|
case "${{ github.ref_name }}" in
|
||||||
|
rc) STABILITY="release-candidate" ;;
|
||||||
|
alpha) STABILITY="alpha" ;;
|
||||||
|
beta) STABILITY="beta" ;;
|
||||||
|
*) STABILITY="development" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
|
fi
|
||||||
|
|
||||||
case "$STABILITY" in
|
case "$STABILITY" in
|
||||||
development) SUFFIX="-dev"; TAG="development" ;;
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
@@ -85,20 +110,26 @@ jobs:
|
|||||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Read current version (bump already handled by push workflow)
|
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
case "$STABILITY" in
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.01"
|
release-candidate) BUMP="minor" ;;
|
||||||
|
*) BUMP="patch" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Strip any existing suffix from version before applying stability
|
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||||
|
|
||||||
|
# Set stability suffix and verify consistency
|
||||||
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||||
|
|
||||||
# Verify version consistency across all files
|
|
||||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
# Update VERSION variable with suffix
|
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
|
||||||
|
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Append suffix for output
|
||||||
if [ -n "$SUFFIX" ]; then
|
if [ -n "$SUFFIX" ]; then
|
||||||
VERSION="${VERSION}${SUFFIX}"
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
fi
|
fi
|
||||||
@@ -142,7 +173,42 @@ jobs:
|
|||||||
php ${MOKO_CLI}/release_create.php \
|
php ${MOKO_CLI}/release_create.php \
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--repo "${GITEA_REPO}" --branch dev --prerelease
|
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||||
|
|
||||||
|
- name: Update release notes from CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||||
|
else
|
||||||
|
NOTES="Release ${VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update release body via API
|
||||||
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import json, urllib.request
|
||||||
|
body = open('/dev/stdin').read()
|
||||||
|
payload = json.dumps({'body': body}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
|
data=payload, method='PATCH',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
" <<< "$NOTES"
|
||||||
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build package and upload
|
- name: Build package and upload
|
||||||
id: package
|
id: package
|
||||||
@@ -155,55 +221,8 @@ jobs:
|
|||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--repo "${GITEA_REPO}" --output /tmp || true
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
- name: Update updates.xml
|
# updates.xml is generated dynamically by MokoGitea license server
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
# No need to build, commit, or sync updates.xml from workflows
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
|
||||||
|
|
||||||
if [ ! -f "updates.xml" ]; then
|
|
||||||
echo "No updates.xml -- skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
SHA_FLAG=""
|
|
||||||
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
|
||||||
|
|
||||||
php ${MOKO_CLI}/updates_xml_build.php \
|
|
||||||
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
|
||||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
|
||||||
${SHA_FLAG}
|
|
||||||
|
|
||||||
# Commit and push
|
|
||||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git add updates.xml
|
|
||||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
|
||||||
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: "Sync updates.xml to all branches"
|
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
CURRENT_BRANCH="${{ github.ref_name }}"
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
|
|
||||||
for BRANCH in main dev; do
|
|
||||||
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
|
||||||
echo "Syncing updates.xml -> ${BRANCH}"
|
|
||||||
git fetch origin "${BRANCH}" 2>/dev/null || continue
|
|
||||||
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
|
|
||||||
git checkout "${CURRENT_BRANCH}" -- updates.xml
|
|
||||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
|
||||||
git add updates.xml
|
|
||||||
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
|
|
||||||
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
|
|
||||||
fi
|
|
||||||
git checkout "${CURRENT_BRANCH}" 2>/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: "Delete lesser pre-release channels (cascade)"
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
# INGROUP: moko-platform.Validation
|
# INGROUP: moko-platform.Validation
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
# PATH: /templates/workflows/joomla/repo_health.yml.template
|
||||||
# VERSION: 04.06.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
|
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
name: "Generic: Repo Health"
|
name: "Generic: Repo Health"
|
||||||
@@ -24,13 +24,12 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
profile:
|
profile:
|
||||||
description: 'Validation profile: all, release, scripts, or repo'
|
description: 'Validation profile: all, scripts, or repo'
|
||||||
required: true
|
required: true
|
||||||
default: all
|
default: all
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- all
|
- all
|
||||||
- release
|
|
||||||
- scripts
|
- scripts
|
||||||
- repo
|
- repo
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -40,10 +39,6 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Release policy - Repository Variables Only
|
|
||||||
RELEASE_REQUIRED_REPO_VARS: RS_FTP_PATH_SUFFIX
|
|
||||||
RELEASE_OPTIONAL_REPO_VARS: DEV_FTP_SUFFIX
|
|
||||||
|
|
||||||
# Scripts governance policy
|
# Scripts governance policy
|
||||||
SCRIPTS_REQUIRED_DIRS:
|
SCRIPTS_REQUIRED_DIRS:
|
||||||
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
|
||||||
@@ -138,101 +133,6 @@ jobs:
|
|||||||
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
|
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
release_config:
|
|
||||||
name: Release configuration
|
|
||||||
needs: access_check
|
|
||||||
if: ${{ needs.access_check.outputs.allowed == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 20
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Guardrails release vars
|
|
||||||
env:
|
|
||||||
PROFILE_RAW: ${{ github.event.inputs.profile }}
|
|
||||||
RS_FTP_PATH_SUFFIX: ${{ vars.RS_FTP_PATH_SUFFIX }}
|
|
||||||
DEV_FTP_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
profile="${PROFILE_RAW:-all}"
|
|
||||||
case "${profile}" in
|
|
||||||
all|release|scripts|repo) ;;
|
|
||||||
*)
|
|
||||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ "${profile}" = 'scripts' ] || [ "${profile}" = 'repo' ]; then
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Release configuration (Repository Variables)'
|
|
||||||
printf '%s\n' "Profile: ${profile}"
|
|
||||||
printf '%s\n' 'Status: SKIPPED'
|
|
||||||
printf '%s\n' 'Reason: profile excludes release validation'
|
|
||||||
printf '\n'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
IFS=',' read -r -a required <<< "${RELEASE_REQUIRED_REPO_VARS}"
|
|
||||||
IFS=',' read -r -a optional <<< "${RELEASE_OPTIONAL_REPO_VARS}"
|
|
||||||
|
|
||||||
missing=()
|
|
||||||
missing_optional=()
|
|
||||||
|
|
||||||
for k in "${required[@]}"; do
|
|
||||||
v="${!k:-}"
|
|
||||||
[ -z "${v}" ] && missing+=("${k}")
|
|
||||||
done
|
|
||||||
|
|
||||||
for k in "${optional[@]}"; do
|
|
||||||
v="${!k:-}"
|
|
||||||
[ -z "${v}" ] && missing_optional+=("${k}")
|
|
||||||
done
|
|
||||||
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Release configuration (Repository Variables)'
|
|
||||||
printf '%s\n' "Profile: ${profile}"
|
|
||||||
printf '%s\n' '| Variable | Status |'
|
|
||||||
printf '%s\n' '|---|---|'
|
|
||||||
printf '%s\n' "| RS_FTP_PATH_SUFFIX | ${RS_FTP_PATH_SUFFIX:-NOT SET} |"
|
|
||||||
printf '%s\n' "| DEV_FTP_SUFFIX | ${DEV_FTP_SUFFIX:-NOT SET} |"
|
|
||||||
printf '\n'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
if [ "${#missing_optional[@]}" -gt 0 ]; then
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Missing optional repository variables'
|
|
||||||
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
|
|
||||||
printf '\n'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${#missing[@]}" -gt 0 ]; then
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Missing required repository variables'
|
|
||||||
for m in "${missing[@]}"; do printf '%s\n' "- ${m}"; done
|
|
||||||
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository variables.'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
{
|
|
||||||
printf '%s\n' '### Repository variables validation result'
|
|
||||||
printf '%s\n' 'Status: OK'
|
|
||||||
printf '%s\n' 'All required repository variables present.'
|
|
||||||
printf '%s\n' ''
|
|
||||||
printf '%s\n' '**Note**: Organization secrets (RS_FTP_HOST, RS_FTP_USER, etc.) are validated at deployment time, not in repository health checks.'
|
|
||||||
printf '\n'
|
|
||||||
} >> "${GITHUB_STEP_SUMMARY}"
|
|
||||||
|
|
||||||
scripts_governance:
|
scripts_governance:
|
||||||
name: Scripts governance
|
name: Scripts governance
|
||||||
needs: access_check
|
needs: access_check
|
||||||
@@ -256,14 +156,14 @@ jobs:
|
|||||||
|
|
||||||
profile="${PROFILE_RAW:-all}"
|
profile="${PROFILE_RAW:-all}"
|
||||||
case "${profile}" in
|
case "${profile}" in
|
||||||
all|release|scripts|repo) ;;
|
all|scripts|repo) ;;
|
||||||
*)
|
*)
|
||||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "${profile}" = 'release' ] || [ "${profile}" = 'repo' ]; then
|
if [ "${profile}" = 'repo' ]; then
|
||||||
{
|
{
|
||||||
printf '%s\n' '### Scripts governance'
|
printf '%s\n' '### Scripts governance'
|
||||||
printf '%s\n' "Profile: ${profile}"
|
printf '%s\n' "Profile: ${profile}"
|
||||||
@@ -370,14 +270,14 @@ jobs:
|
|||||||
|
|
||||||
profile="${PROFILE_RAW:-all}"
|
profile="${PROFILE_RAW:-all}"
|
||||||
case "${profile}" in
|
case "${profile}" in
|
||||||
all|release|scripts|repo) ;;
|
all|scripts|repo) ;;
|
||||||
*)
|
*)
|
||||||
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "${profile}" = 'release' ] || [ "${profile}" = 'scripts' ]; then
|
if [ "${profile}" = 'scripts' ]; then
|
||||||
{
|
{
|
||||||
printf '%s\n' '### Repository health'
|
printf '%s\n' '### Repository health'
|
||||||
printf '%s\n' "Profile: ${profile}"
|
printf '%s\n' "Profile: ${profile}"
|
||||||
@@ -704,7 +604,7 @@ jobs:
|
|||||||
printf '%s\n' '| Domain | Status | Notes |'
|
printf '%s\n' '| Domain | Status | Notes |'
|
||||||
printf '%s\n' '|---|---|---|'
|
printf '%s\n' '|---|---|---|'
|
||||||
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
|
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
|
||||||
printf '%s\n' '| Release variables | OK | Repository variables validation |'
|
printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |'
|
||||||
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
|
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
|
||||||
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
|
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
|
||||||
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
|
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
|
||||||
@@ -767,3 +667,45 @@ jobs:
|
|||||||
echo "### Site Health" >> $GITHUB_STEP_SUMMARY
|
echo "### Site Health" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
|
echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
# Issue Reporter — file issues for failed gates
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
report-issues:
|
||||||
|
name: "Report Issues"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [access_check, scripts_governance, repo_health]
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
(needs.scripts_governance.result == 'failure' ||
|
||||||
|
needs.repo_health.result == 'failure')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: automation/ci-issue-reporter.sh
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: "File issues for failed gates"
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
chmod +x automation/ci-issue-reporter.sh
|
||||||
|
REPORTER="./automation/ci-issue-reporter.sh"
|
||||||
|
WF="Repo Health"
|
||||||
|
|
||||||
|
report_gate() {
|
||||||
|
local gate="$1" result="$2" details="$3"
|
||||||
|
if [ "$result" = "failure" ]; then
|
||||||
|
"$REPORTER" --gate "$gate" --details "$details" --workflow "$WF" --severity error
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
report_gate "Scripts Governance" \
|
||||||
|
"${{ needs.scripts_governance.result }}" \
|
||||||
|
"Scripts directory policy violations detected. Review required and allowed directories."
|
||||||
|
|
||||||
|
report_gate "Repository Health" \
|
||||||
|
"${{ needs.repo_health.result }}" \
|
||||||
|
"Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Security
|
# INGROUP: MokoStandards.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
# PATH: /.gitea/workflows/security-audit.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||||
@@ -80,19 +80,3 @@ jobs:
|
|||||||
-H "Priority: high" \
|
-H "Priority: high" \
|
||||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
-d "Security audit found vulnerabilities. Review dependency updates." \
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
|
|
||||||
|
|
||||||
- name: Joomla version audit
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
|
|
||||||
echo "$JOOMLA_SITES" > /tmp/sites.json
|
|
||||||
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
|
|
||||||
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
|
|
||||||
rm -f /tmp/sites.json
|
|
||||||
else
|
|
||||||
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,312 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /templates/workflows/update-server.yml
|
|
||||||
# VERSION: 05.00.00
|
|
||||||
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
|
|
||||||
#
|
|
||||||
# Thin wrapper around moko-platform CLI tools.
|
|
||||||
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
|
|
||||||
#
|
|
||||||
# Joomla filters update entries by the user's "Minimum Stability" setting.
|
|
||||||
|
|
||||||
name: "Update Server"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
stability:
|
|
||||||
description: 'Stability tag'
|
|
||||||
required: true
|
|
||||||
default: 'development'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- alpha
|
|
||||||
- beta
|
|
||||||
- rc
|
|
||||||
- stable
|
|
||||||
|
|
||||||
env:
|
|
||||||
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:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-xml:
|
|
||||||
name: Update Server
|
|
||||||
runs-on: release
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
|
||||||
env:
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
||||||
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
|
||||||
run: |
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
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
|
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
|
||||||
rm -rf /tmp/moko-platform
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
|
||||||
/tmp/moko-platform 2>/dev/null || true
|
|
||||||
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
|
|
||||||
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Detect platform
|
|
||||||
id: platform
|
|
||||||
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
|
||||||
|
|
||||||
- name: Resolve stability and bump version
|
|
||||||
id: meta
|
|
||||||
run: |
|
|
||||||
BRANCH="${{ github.ref_name }}"
|
|
||||||
|
|
||||||
# Configure git for bot pushes
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
|
|
||||||
# Auto-bump patch version
|
|
||||||
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
|
|
||||||
|
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
|
||||||
|
|
||||||
# Strip any existing suffix before applying stability
|
|
||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
|
||||||
|
|
||||||
# Determine stability from branch or manual input
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
STABILITY="${{ inputs.stability }}"
|
|
||||||
elif [[ "$BRANCH" == rc/* ]]; then
|
|
||||||
STABILITY="rc"
|
|
||||||
elif [[ "$BRANCH" == beta/* ]]; then
|
|
||||||
STABILITY="beta"
|
|
||||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
|
||||||
STABILITY="alpha"
|
|
||||||
else
|
|
||||||
STABILITY="development"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Version suffix per stability stream
|
|
||||||
case "$STABILITY" in
|
|
||||||
development) SUFFIX="-dev"; TAG="development" ;;
|
|
||||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
|
||||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
|
||||||
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
|
|
||||||
*) SUFFIX=""; TAG="stable" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Propagate version with stability suffix to all manifest files
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
|
||||||
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
|
||||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
|
||||||
|
|
||||||
# Re-read version (now includes suffix from version_set_platform)
|
|
||||||
if [ -n "$SUFFIX" ]; then
|
|
||||||
VERSION="${VERSION}${SUFFIX}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Commit version bump if changed
|
|
||||||
git add -A
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
|
|
||||||
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
|
||||||
git push
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Create release and upload package
|
|
||||||
id: package
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# Create or update Gitea release
|
|
||||||
php ${MOKO_CLI}/release_create.php \
|
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
|
||||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
|
||||||
|
|
||||||
# Build package and upload
|
|
||||||
php ${MOKO_CLI}/release_package.php \
|
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
|
||||||
--repo "${GITEA_REPO}" --output /tmp || true
|
|
||||||
|
|
||||||
- name: Update updates.xml
|
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
|
||||||
|
|
||||||
if [ ! -f "updates.xml" ]; then
|
|
||||||
echo "No updates.xml — skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
SHA_FLAG=""
|
|
||||||
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
|
||||||
|
|
||||||
php ${MOKO_CLI}/updates_xml_build.php \
|
|
||||||
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
|
||||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
|
||||||
${SHA_FLAG}
|
|
||||||
|
|
||||||
# Commit and push updates.xml
|
|
||||||
git add updates.xml
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
|
||||||
git push
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Sync updates.xml to main
|
|
||||||
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_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
|
|
||||||
python3 -c "
|
|
||||||
import base64, json, urllib.request, sys
|
|
||||||
with open('updates.xml', 'rb') as f:
|
|
||||||
content = base64.b64encode(f.read()).decode()
|
|
||||||
payload = json.dumps({
|
|
||||||
'content': content,
|
|
||||||
'sha': '${FILE_SHA}',
|
|
||||||
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
|
|
||||||
'branch': 'main'
|
|
||||||
}).encode()
|
|
||||||
req = urllib.request.Request(
|
|
||||||
'${API_BASE}/contents/updates.xml',
|
|
||||||
data=payload, method='PUT',
|
|
||||||
headers={
|
|
||||||
'Authorization': 'token ${GITEA_TOKEN}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
})
|
|
||||||
try:
|
|
||||||
urllib.request.urlopen(req)
|
|
||||||
print('updates.xml synced to main')
|
|
||||||
except Exception as e:
|
|
||||||
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: SFTP deploy to dev server
|
|
||||||
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
|
||||||
env:
|
|
||||||
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
|
||||||
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
|
||||||
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
|
||||||
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
|
||||||
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
|
||||||
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
|
||||||
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
# Permission check: admin or maintain role required
|
|
||||||
ACTOR="${{ github.actor }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
|
||||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
|
||||||
case "$PERMISSION" in
|
|
||||||
admin|maintain|write) ;;
|
|
||||||
*)
|
|
||||||
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
|
||||||
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
|
||||||
|
|
||||||
PORT="${DEV_PORT:-22}"
|
|
||||||
REMOTE="${DEV_PATH%/}"
|
|
||||||
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
|
||||||
|
|
||||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
|
||||||
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
|
||||||
if [ -n "$DEV_KEY" ]; then
|
|
||||||
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
|
||||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
|
||||||
else
|
|
||||||
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
|
||||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
|
||||||
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
|
||||||
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
|
||||||
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
DISPLAY="${{ steps.meta.outputs.display_version }}"
|
|
||||||
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
+13
-52
@@ -8,65 +8,26 @@
|
|||||||
DEFGROUP: Joomla.Template.Site
|
DEFGROUP: Joomla.Template.Site
|
||||||
INGROUP: MokoOnyx.Documentation
|
INGROUP: MokoOnyx.Documentation
|
||||||
PATH: ./CHANGELOG.md
|
PATH: ./CHANGELOG.md
|
||||||
<<<<<<< HEAD
|
VERSION: 02.21.05
|
||||||
<<<<<<< HEAD
|
|
||||||
VERSION: 02.15.02
|
|
||||||
BRIEF: Changelog file documenting version history of MokoOnyx
|
BRIEF: Changelog file documenting version history of MokoOnyx
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Changelog — MokoOnyx (VERSION: 02.15.02)
|
# Changelog — MokoOnyx (VERSION: 02.21.05)
|
||||||
=======
|
|
||||||
VERSION: 02.15.02
|
|
||||||
BRIEF: Changelog file documenting version history of MokoOnyx
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Changelog — MokoOnyx (VERSION: 02.15.02)
|
|
||||||
>>>>>>> origin/main
|
|
||||||
=======
|
|
||||||
VERSION: 02.15.02
|
|
||||||
BRIEF: Changelog file documenting version history of MokoOnyx
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Changelog — MokoOnyx (VERSION: 02.15.02)
|
|
||||||
>>>>>>> origin/main
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [02.15.00] --- 2026-05-30
|
### Fixed
|
||||||
|
- Strip Joomla-injected `p-2` padding class from Font Awesome icons in all menu overrides (default, mainmenu, horizontal)
|
||||||
=======
|
|
||||||
VERSION: 02.15.02
|
|
||||||
BRIEF: Changelog file documenting version history of MokoOnyx
|
|
||||||
-->
|
|
||||||
|
|
||||||
# Changelog — MokoOnyx (VERSION: 02.15.02)
|
|
||||||
>>>>>>> origin/main
|
|
||||||
## [02.14.00] --- 2026-05-30
|
|
||||||
|
|
||||||
## [02.13.00] --- 2026-05-30
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Hero image (`hero.jpg`) to template images
|
|
||||||
- `.fa-solid`, `.fa-regular`, `.fa-brands`, `.fa-light` icon margin spacing
|
|
||||||
- `.blog-item .item-image` fixed 250px height with object-fit cover
|
|
||||||
- Hide header on home page option (`hideHeaderHome` template param)
|
|
||||||
- Hide main menu on home page option (`hideMenuHome` template param)
|
|
||||||
- Three distinct menu overrides: mainmenu (collapsible), horizontal (always visible), default (vertical)
|
|
||||||
- `<php_minimum>8.1.0</php_minimum>` to templateDetails.xml
|
|
||||||
- `<changelogurl>` support in updates.xml
|
|
||||||
- CONTRIBUTING.md with universal workflow and version policy
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Release pipeline rework: independent update streams, CLI-driven workflows
|
- Migrated update server URL from raw file endpoint to Gitea Pages
|
||||||
- Version bumps only trigger on `src/` changes (not docs/config)
|
- Release workflow no longer manages updates.xml (decoupled to Gitea Pages)
|
||||||
- Branch protection: CI bot only for push, force push disabled
|
- Added conflict-marker guard to PR check and release workflows
|
||||||
- Auto-bump supports dev, rc, feature/*, patch/* branches
|
- Added Joomla language file validation (syntax, duplicates, en-GB/en-US consistency)
|
||||||
|
- Added JEXEC guard, joomla.asset.json, XML well-formedness, and script file CI checks
|
||||||
|
- Removed RS_FTP_PATH_SUFFIX from repo health requirements
|
||||||
|
|
||||||
### Fixed
|
## [02.20.00] --- 2026-06-04
|
||||||
- Joomla update loop caused by version mismatch between ZIP and updates.xml
|
|
||||||
- Duplicate hamburger menu on mobile
|
|
||||||
- Stacked version suffixes (-dev-dev-dev)
|
|
||||||
- Template name doubling (Template - Template - MokoOnyx)
|
|
||||||
|
|
||||||
## [02.08.00] --- 2026-05-29
|
## [02.18.00] --- 2026-06-02
|
||||||
|
|
||||||
## [02.08.00] --- 2026-05-29
|
## [02.15.00] --- 2026-05-30
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code when working with this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
**MokoOnyx** -- MokoOnyx - Joomla site template (successor to MokoCassiopeia)
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|---|---|
|
|
||||||
| **Platform** | joomla |
|
|
||||||
| **Language** | PHP |
|
|
||||||
| **Default branch** | main |
|
|
||||||
| **License** | GPL-3.0-or-later |
|
|
||||||
| **Wiki** | [MokoOnyx Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/wiki) |
|
|
||||||
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
|
|
||||||
|
|
||||||
## Common Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
composer install # Install PHP dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
This is a Joomla extension. Key directories:
|
|
||||||
- `src/` -- extension source (deployed to Joomla)
|
|
||||||
- `src/*.xml` -- manifest file (version, files, params)
|
|
||||||
- `src/src/` or `src/services/` -- PHP classes
|
|
||||||
- `src/language/` -- translation strings
|
|
||||||
- `src/media/` -- CSS/JS/images
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
|
||||||
|
|
||||||
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
|
|
||||||
- **Attribution**: use `Authored-by: Moko Consulting` in commits
|
|
||||||
- **Branch strategy**: develop on `dev`, merge to `main` for release
|
|
||||||
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates)
|
|
||||||
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
|
|
||||||
- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
|
||||||
+1
-11
@@ -10,17 +10,7 @@
|
|||||||
INGROUP: MokoOnyx.Governance
|
INGROUP: MokoOnyx.Governance
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
FILE: SECURITY.md
|
FILE: SECURITY.md
|
||||||
<<<<<<< HEAD
|
VERSION: 02.21.05
|
||||||
<<<<<<< HEAD
|
|
||||||
VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
VERSION: 02.15.02
|
|
||||||
>>>>>>> origin/main
|
|
||||||
>>>>>>> origin/main
|
|
||||||
BRIEF: Security policy and vulnerability reporting process for MokoOnyx.
|
BRIEF: Security policy and vulnerability reporting process for MokoOnyx.
|
||||||
PATH: /SECURITY.md
|
PATH: /SECURITY.md
|
||||||
NOTE: This policy is process oriented and does not replace secure engineering practices.
|
NOTE: This policy is process oriented and does not replace secure engineering practices.
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Automation.CI
|
||||||
|
# INGROUP: moko-platform.Automation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /automation/ci-issue-reporter.sh
|
||||||
|
# VERSION: 09.23.00
|
||||||
|
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
|
||||||
|
# Deduplicates by searching open issues with the "ci-auto" label
|
||||||
|
# whose title matches the gate. If a matching issue exists, a comment
|
||||||
|
# is appended instead of opening a duplicate.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||||
|
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
|
||||||
|
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
||||||
|
REPO="${GITHUB_REPOSITORY:-}"
|
||||||
|
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
|
||||||
|
LABEL_NAME="ci-auto"
|
||||||
|
LABEL_COLOR="#e11d48"
|
||||||
|
|
||||||
|
GATE=""
|
||||||
|
DETAILS=""
|
||||||
|
SEVERITY="error"
|
||||||
|
WORKFLOW=""
|
||||||
|
|
||||||
|
# ── Parse arguments ─────────────────────────────────────────────────────────
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
|
||||||
|
|
||||||
|
Required:
|
||||||
|
--gate CI gate name (e.g. "Code Quality", "Self-Health")
|
||||||
|
--details Human-readable failure description
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--severity "error" (default) or "warning"
|
||||||
|
--workflow Workflow name for the issue title
|
||||||
|
--repo owner/repo (default: \$GITHUB_REPOSITORY)
|
||||||
|
--run-url URL to the CI run (auto-detected from env)
|
||||||
|
--token Gitea API token (default: \$GITEA_TOKEN)
|
||||||
|
--url Gitea base URL (default: \$GITEA_URL)
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--gate) GATE="$2"; shift 2 ;;
|
||||||
|
--details) DETAILS="$2"; shift 2 ;;
|
||||||
|
--severity) SEVERITY="$2"; shift 2 ;;
|
||||||
|
--workflow) WORKFLOW="$2"; shift 2 ;;
|
||||||
|
--repo) REPO="$2"; shift 2 ;;
|
||||||
|
--run-url) RUN_URL="$2"; shift 2 ;;
|
||||||
|
--token) GITEA_TOKEN="$2"; shift 2 ;;
|
||||||
|
--url) GITEA_URL="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
*) echo "Unknown option: $1"; usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
|
||||||
|
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
|
||||||
|
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
|
||||||
|
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
|
||||||
|
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${REPO}"
|
||||||
|
|
||||||
|
# ── Build title ─────────────────────────────────────────────────────────────
|
||||||
|
if [[ -n "$WORKFLOW" ]]; then
|
||||||
|
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
|
||||||
|
else
|
||||||
|
TITLE="[CI] ${GATE} failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Ensure label exists ─────────────────────────────────────────────────────
|
||||||
|
ensure_label() {
|
||||||
|
local exists
|
||||||
|
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [[ "$exists" == "200" ]]; then
|
||||||
|
# Check if label already exists
|
||||||
|
local found
|
||||||
|
found=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null \
|
||||||
|
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
|
||||||
|
|
||||||
|
if [[ -z "$found" ]]; then
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/labels" \
|
||||||
|
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
|
||||||
|
> /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Search for existing open issue ──────────────────────────────────────────
|
||||||
|
find_existing_issue() {
|
||||||
|
# URL-encode the gate name for the query
|
||||||
|
local query
|
||||||
|
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
|
||||||
|
2>/dev/null || echo "[]")
|
||||||
|
|
||||||
|
# Extract the first matching issue number
|
||||||
|
echo "$response" \
|
||||||
|
| grep -oP '"number":\s*\K[0-9]+' \
|
||||||
|
| head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Build issue body ────────────────────────────────────────────────────────
|
||||||
|
build_body() {
|
||||||
|
local severity_badge
|
||||||
|
if [[ "$SEVERITY" == "error" ]]; then
|
||||||
|
severity_badge="**Severity:** Error"
|
||||||
|
else
|
||||||
|
severity_badge="**Severity:** Warning"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<BODY
|
||||||
|
## CI Gate Failure: ${GATE}
|
||||||
|
|
||||||
|
${severity_badge}
|
||||||
|
**Workflow:** ${WORKFLOW:-unknown}
|
||||||
|
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||||
|
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||||
|
**Run:** [View CI run](${RUN_URL})
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
${DETAILS}
|
||||||
|
|
||||||
|
### Resolution
|
||||||
|
|
||||||
|
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
|
||||||
|
BODY
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Build comment body (for existing issues) ────────────────────────────────
|
||||||
|
build_comment() {
|
||||||
|
cat <<COMMENT
|
||||||
|
### CI failure recurrence
|
||||||
|
|
||||||
|
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||||
|
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||||
|
**Run:** [View CI run](${RUN_URL})
|
||||||
|
|
||||||
|
${DETAILS}
|
||||||
|
COMMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main ────────────────────────────────────────────────────────────────────
|
||||||
|
ensure_label
|
||||||
|
|
||||||
|
EXISTING=$(find_existing_issue)
|
||||||
|
|
||||||
|
if [[ -n "$EXISTING" ]]; then
|
||||||
|
# Append comment to existing issue
|
||||||
|
COMMENT_BODY=$(build_comment)
|
||||||
|
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
|
||||||
|
|
||||||
|
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${EXISTING}/comments" \
|
||||||
|
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [[ "$HTTP" == "201" ]]; then
|
||||||
|
echo "Commented on existing issue #${EXISTING}"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Create new issue
|
||||||
|
ISSUE_BODY=$(build_body)
|
||||||
|
ISSUE_JSON=$(python3 -c "
|
||||||
|
import sys, json
|
||||||
|
body = sys.stdin.read()
|
||||||
|
print(json.dumps({
|
||||||
|
'title': sys.argv[1],
|
||||||
|
'body': body,
|
||||||
|
'labels': []
|
||||||
|
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
|
||||||
|
|
||||||
|
# Create the issue
|
||||||
|
RESPONSE=$(curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues" \
|
||||||
|
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
|
||||||
|
|
||||||
|
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
|
||||||
|
|
||||||
|
if [[ -n "$ISSUE_NUM" ]]; then
|
||||||
|
# Apply label (separate call — more reliable across Gitea versions)
|
||||||
|
LABEL_ID=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null \
|
||||||
|
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
|
||||||
|
| head -1 || true)
|
||||||
|
|
||||||
|
if [[ -n "$LABEL_ID" ]]; then
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${ISSUE_NUM}/labels" \
|
||||||
|
-d "{\"labels\":[${LABEL_ID}]}" \
|
||||||
|
> /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to create issue"
|
||||||
|
echo "Response: ${RESPONSE}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -236,11 +236,11 @@ use Joomla\CMS\Log\Log;
|
|||||||
|
|
||||||
// Update the update server
|
// Update the update server
|
||||||
try {
|
try {
|
||||||
$onyxUpdatesUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml';
|
$onyxUpdatesUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/updates.xml';
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->update('#__update_sites')
|
->update('#__update_sites')
|
||||||
->set($db->quoteName('location') . ' = ' . $db->quote($onyxUpdatesUrl))
|
->set($db->quoteName('location') . ' = ' . $db->quote($onyxUpdatesUrl))
|
||||||
->set($db->quoteName('name') . ' = ' . $db->quote($newDisplay))
|
->set($db->quoteName('name') . ' = ' . $db->quote('Template - MokoOnyx'))
|
||||||
->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
|
->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
|
||||||
$db->setQuery($query)->execute();
|
$db->setQuery($query)->execute();
|
||||||
$n = $db->getAffectedRows();
|
$n = $db->getAffectedRows();
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class MokoMinifyHelper
|
|||||||
$js = preg_replace('/\s*([{}();,=+\-*\/<>!&|?:])\s*/', '$1', $js);
|
$js = preg_replace('/\s*([{}();,=+\-*\/<>!&|?:])\s*/', '$1', $js);
|
||||||
|
|
||||||
// Restore necessary spaces (after keywords)
|
// Restore necessary spaces (after keywords)
|
||||||
$js = preg_replace('/(var|let|const|return|typeof|instanceof|new|delete|throw|case|in|of)([^\s;})><=!&|?:,])/', '$1 $2', $js);
|
$js = preg_replace('/\b(var|let|const|return|typeof|instanceof|new|delete|throw|case|in|of)\b([^\s;})><=!&|?:,])/', '$1 $2', $js);
|
||||||
|
|
||||||
return trim($js);
|
return trim($js);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
*
|
|
||||||
* This file is part of a Moko Consulting project.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* FILE INFORMATION
|
|
||||||
* DEFGROUP: MokoOnyx.Override
|
|
||||||
* INGROUP: MokoOnyx
|
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
|
||||||
* PATH: /html/com_joomgallery/category/default.php
|
|
||||||
<<<<<<< HEAD
|
|
||||||
<<<<<<< HEAD
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
>>>>>>> origin/main
|
|
||||||
>>>>>>> origin/main
|
|
||||||
* BRIEF: Category view override — password gate then loads default_cat sub-layout
|
|
||||||
*/
|
|
||||||
|
|
||||||
// No direct access
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Router\Route;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\HTML\HTMLHelper;
|
|
||||||
|
|
||||||
// Import CSS & JS
|
|
||||||
$wa = $this->document->getWebAssetManager();
|
|
||||||
$wa->useStyle('com_joomgallery.site');
|
|
||||||
$wa->useStyle('com_joomgallery.jg-icon-font');
|
|
||||||
?>
|
|
||||||
|
|
||||||
<?php if ($this->item->pw_protected) : ?>
|
|
||||||
<div class="com-joomgallery-category--locked">
|
|
||||||
<form action="<?php echo Route::_('index.php?task=category.unlock&catid=' . $this->item->id); ?>" method="post" class="row g-3 align-items-end" autocomplete="off">
|
|
||||||
<div class="col-12">
|
|
||||||
<h3><i class="jg-icon-lock me-2" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_CATEGORY_PASSWORD_PROTECTED'); ?></h3>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<label for="jg_password" class="form-label"><?php echo Text::_('JGLOBAL_PASSWORD'); ?></label>
|
|
||||||
<input type="password" name="password" id="jg_password" class="form-control" required />
|
|
||||||
</div>
|
|
||||||
<div class="col-auto">
|
|
||||||
<button type="submit" class="btn btn-primary" id="jg_unlock_button">
|
|
||||||
<i class="jg-icon-unlock me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_CATEGORY_BUTTON_UNLOCK'); ?>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<?php echo HTMLHelper::_('form.token'); ?>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<?php else : ?>
|
|
||||||
<?php echo $this->loadTemplate('cat'); ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
*
|
|
||||||
* This file is part of a Moko Consulting project.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* FILE INFORMATION
|
|
||||||
* DEFGROUP: MokoOnyx.Override
|
|
||||||
* INGROUP: MokoOnyx
|
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
|
||||||
* PATH: /html/com_joomgallery/category/default_cat.php
|
|
||||||
<<<<<<< HEAD
|
|
||||||
<<<<<<< HEAD
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
>>>>>>> origin/main
|
|
||||||
>>>>>>> origin/main
|
|
||||||
* BRIEF: Category sub-layout — subcategories grid + images grid with pagination
|
|
||||||
*/
|
|
||||||
|
|
||||||
// No direct access
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Router\Route;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\Session\Session;
|
|
||||||
use Joomla\CMS\HTML\HTMLHelper;
|
|
||||||
use Joomla\CMS\Layout\LayoutHelper;
|
|
||||||
use Joomgallery\Component\Joomgallery\Administrator\Helper\JoomHelper;
|
|
||||||
|
|
||||||
// Image params
|
|
||||||
$image_type = $this->params['configs']->get('jg_category_view_type_image', 'thumbnail', 'STRING');
|
|
||||||
$gallery_class = $this->params['configs']->get('jg_category_view_class', 'masonry', 'STRING');
|
|
||||||
$num_columns = $this->params['configs']->get('jg_category_view_num_columns', 3, 'INT');
|
|
||||||
$image_class = $this->params['configs']->get('jg_category_view_image_class', 0, 'INT');
|
|
||||||
$justified_height = $this->params['configs']->get('jg_category_view_justified_height', 200, 'INT');
|
|
||||||
$justified_gap = $this->params['configs']->get('jg_category_view_justified_gap', 5, 'INT');
|
|
||||||
$image_link = $this->params['configs']->get('jg_category_view_image_link', 'defaultview', 'STRING');
|
|
||||||
$lightbox_image = $this->params['configs']->get('jg_category_view_lightbox_image', 'detail', 'STRING');
|
|
||||||
$pagination_type = $this->params['configs']->get('jg_category_view_pagination', 0, 'INT');
|
|
||||||
$show_subcategories = $this->params['configs']->get('jg_category_view_subcategories', 1, 'INT');
|
|
||||||
$subcategory_type_image = $this->params['configs']->get('jg_category_view_type_subcategory_image', 'thumbnail', 'STRING');
|
|
||||||
$num_columns_subcats = $this->params['configs']->get('jg_category_view_subcategories_num_columns', 3, 'INT');
|
|
||||||
|
|
||||||
// Import CSS & JS
|
|
||||||
$wa = $this->document->getWebAssetManager();
|
|
||||||
|
|
||||||
if ($gallery_class == 'masonry') {
|
|
||||||
$wa->useScript('com_joomgallery.masonry');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($gallery_class == 'justified') {
|
|
||||||
$wa->useScript('com_joomgallery.justified');
|
|
||||||
$wa->addInlineStyle('.jg-images[class*=" justified-"] .jg-image-caption-hover { right: ' . $justified_gap . 'px; }');
|
|
||||||
}
|
|
||||||
|
|
||||||
$lightbox = false;
|
|
||||||
if ($image_link == 'lightgallery') {
|
|
||||||
$lightbox = true;
|
|
||||||
$wa->useScript('com_joomgallery.lightgallery');
|
|
||||||
$wa->useScript('com_joomgallery.lg-thumbnail');
|
|
||||||
$wa->useStyle('com_joomgallery.lightgallery-bundle');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialise the grid script
|
|
||||||
$iniJS = 'window.joomGrid = {';
|
|
||||||
$iniJS .= ' itemid: ' . $this->item->id . ',';
|
|
||||||
$iniJS .= ' pagination: ' . $pagination_type . ',';
|
|
||||||
$iniJS .= ' layout: "' . $gallery_class . '",';
|
|
||||||
$iniJS .= ' num_columns: ' . $num_columns . ',';
|
|
||||||
$iniJS .= ' lightbox: ' . ($lightbox ? 'true' : 'false') . ',';
|
|
||||||
$iniJS .= ' justified: {height: ' . $justified_height . ', gap: ' . $justified_gap . '}';
|
|
||||||
$iniJS .= '};';
|
|
||||||
|
|
||||||
$wa->addInlineScript($iniJS, ['position' => 'before'], [], ['com_joomgallery.joomgrid']);
|
|
||||||
$wa->useScript('com_joomgallery.joomgrid');
|
|
||||||
|
|
||||||
// Access check
|
|
||||||
$canEdit = $this->getAcl()->checkACL('edit', 'com_joomgallery.category', $this->item->id, $this->item->parent_id, true);
|
|
||||||
$canAdd = $this->getAcl()->checkACL('add', 'com_joomgallery.image', 0, $this->item->id, true);
|
|
||||||
$canDelete = $this->getAcl()->checkACL('delete', 'com_joomgallery.category', $this->item->id, $this->item->parent_id, true);
|
|
||||||
$canCheckin = $this->getAcl()->checkACL('editstate', 'com_joomgallery.category', $this->item->id, $this->item->parent_id, true) || $this->item->checked_out == Factory::getUser()->id;
|
|
||||||
$returnURL = base64_encode(JoomHelper::getViewRoute('category', $this->item->id, $this->item->parent_id, $this->item->language, $this->getLayout()));
|
|
||||||
|
|
||||||
$hasSubcats = $show_subcategories && !empty($this->item->children->items) && count($this->item->children->items) > 0;
|
|
||||||
$hasImages = !empty($this->item->images->items) && count($this->item->images->items) > 0;
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="com-joomgallery-category" itemscope itemtype="https://schema.org/ImageGallery">
|
|
||||||
<?php // Page heading ?>
|
|
||||||
<?php if ($this->params['menu']->get('show_page_heading')) : ?>
|
|
||||||
<div class="page-header">
|
|
||||||
<h1><?php echo $this->escape($this->params['menu']->get('page_heading')); ?></h1>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php // Category title ?>
|
|
||||||
<h2 itemprop="name"><?php echo $this->escape($this->item->title); ?></h2>
|
|
||||||
|
|
||||||
<?php // Category description ?>
|
|
||||||
<?php if (!empty($this->item->description)) : ?>
|
|
||||||
<div class="com-joomgallery-category__description mb-3" itemprop="description">
|
|
||||||
<?php echo $this->item->description; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php // Admin buttons ?>
|
|
||||||
<?php if ($canEdit || $canCheckin || $canAdd || $canDelete) : ?>
|
|
||||||
<div class="com-joomgallery-category__actions btn-toolbar mb-3" role="toolbar" aria-label="<?php echo Text::_('JTOOLBAR'); ?>">
|
|
||||||
<?php if ($canCheckin && $this->item->checked_out > 0) : ?>
|
|
||||||
<a class="btn btn-outline-secondary btn-sm me-2" href="<?php echo Route::_('index.php?option=com_joomgallery&task=category.checkin&id=' . $this->item->id . '&return=' . $returnURL . '&' . Session::getFormToken() . '=1'); ?>">
|
|
||||||
<i class="jg-icon-checkin me-1" aria-hidden="true"></i><?php echo Text::_('JLIB_HTML_CHECKIN'); ?>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($canEdit) : ?>
|
|
||||||
<a class="btn btn-outline-primary btn-sm me-2<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="<?php echo Route::_('index.php?option=com_joomgallery&task=category.edit&id=' . $this->item->id . '&return=' . $returnURL); ?>">
|
|
||||||
<i class="jg-icon-edit me-1" aria-hidden="true"></i><?php echo Text::_('JGLOBAL_EDIT'); ?>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($canAdd) : ?>
|
|
||||||
<a class="btn btn-outline-success btn-sm me-2<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="<?php echo Route::_('index.php?option=com_joomgallery&task=image.add&catid=' . $this->item->id . '&return=' . $returnURL); ?>">
|
|
||||||
<i class="jg-icon-upload me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_IMG_UPLOAD_IMAGE'); ?>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($canDelete) : ?>
|
|
||||||
<a class="btn btn-outline-danger btn-sm<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="#deleteCatModal" role="button" data-bs-toggle="modal">
|
|
||||||
<i class="jg-icon-delete me-1" aria-hidden="true"></i><?php echo Text::_('JACTION_DELETE'); ?>
|
|
||||||
</a>
|
|
||||||
<?php echo HTMLHelper::_(
|
|
||||||
'bootstrap.renderModal',
|
|
||||||
'deleteCatModal',
|
|
||||||
[
|
|
||||||
'title' => Text::_('JACTION_DELETE'),
|
|
||||||
'modalWidth' => '50',
|
|
||||||
'bodyHeight' => '100',
|
|
||||||
'footer' => '<button class="btn btn-secondary" data-bs-dismiss="modal">' . Text::_('JCANCEL') . '</button>'
|
|
||||||
. '<a href="' . Route::_('index.php?option=com_joomgallery&task=category.remove&id=' . $this->item->id . '&return=' . $returnURL . '&' . Session::getFormToken() . '=1', false, 2) . '" class="btn btn-danger">' . Text::_('JACTION_DELETE') . '</a>',
|
|
||||||
],
|
|
||||||
Text::_('COM_JOOMGALLERY_COMMON_ALERT_SURE_DELETE_SELECTED_ITEM')
|
|
||||||
); ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php // Subcategories ?>
|
|
||||||
<?php if ($hasSubcats) : ?>
|
|
||||||
<section class="com-joomgallery-category__subcategories mb-4" aria-label="<?php echo Text::_('COM_JOOMGALLERY_SUBCATEGORIES'); ?>">
|
|
||||||
<h3><?php echo Text::_('COM_JOOMGALLERY_SUBCATEGORIES'); ?></h3>
|
|
||||||
<?php
|
|
||||||
$catsData = [
|
|
||||||
'id' => (int) $this->item->id,
|
|
||||||
'items' => $this->item->children->items,
|
|
||||||
'num_columns' => (int) $num_columns_subcats,
|
|
||||||
'image_type' => $subcategory_type_image,
|
|
||||||
];
|
|
||||||
echo LayoutHelper::render('joomgallery.grids.categories', $catsData);
|
|
||||||
?>
|
|
||||||
</section>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php // Images ?>
|
|
||||||
<?php if ($hasImages) : ?>
|
|
||||||
<section class="com-joomgallery-category__images" aria-label="<?php echo Text::_('COM_JOOMGALLERY_IMAGES'); ?>">
|
|
||||||
<?php if ($hasSubcats) : ?>
|
|
||||||
<h3><?php echo Text::_('COM_JOOMGALLERY_IMAGES'); ?></h3>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$imgsData = [
|
|
||||||
'id' => (int) $this->item->id,
|
|
||||||
'layout' => $gallery_class,
|
|
||||||
'items' => $this->item->images->items,
|
|
||||||
'num_columns' => (int) $num_columns,
|
|
||||||
'caption_align' => 'center',
|
|
||||||
'image_class' => $image_class,
|
|
||||||
'image_type' => $image_type,
|
|
||||||
'lightbox_type' => $lightbox_image,
|
|
||||||
'image_link' => $image_link,
|
|
||||||
'image_title' => false,
|
|
||||||
'title_link' => 'defaultview',
|
|
||||||
'image_desc' => false,
|
|
||||||
'image_date' => false,
|
|
||||||
'image_author' => false,
|
|
||||||
'image_tags' => false,
|
|
||||||
];
|
|
||||||
echo LayoutHelper::render('joomgallery.grids.images', $imgsData);
|
|
||||||
?>
|
|
||||||
|
|
||||||
<?php // Pagination ?>
|
|
||||||
<nav class="mt-4" aria-label="<?php echo Text::_('JLIB_HTML_PAGINATION'); ?>">
|
|
||||||
<?php echo $this->item->images->pagination->getListFooter(); ?>
|
|
||||||
</nav>
|
|
||||||
</section>
|
|
||||||
<?php elseif (!$hasSubcats) : ?>
|
|
||||||
<div class="alert alert-info" role="alert">
|
|
||||||
<p class="mb-0"><?php echo Text::_('COM_JOOMGALLERY_GALLERY_NO_IMAGES'); ?></p>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php // Back to parent category ?>
|
|
||||||
<?php if ($this->item->parent_id > 0 && $this->item->parent_id != 1) : ?>
|
|
||||||
<div class="mt-4">
|
|
||||||
<a class="btn btn-outline-secondary" href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=' . (int) $this->item->parent_id); ?>">
|
|
||||||
<i class="jg-icon-arrow-left-alt me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_BACK'); ?>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
if (window.joomGrid.layout != 'justified') {
|
|
||||||
document.querySelectorAll('.' + window.joomGrid.imgclass).forEach(function(image) {
|
|
||||||
image.addEventListener('load', function() {
|
|
||||||
this.closest('.' + window.joomGrid.imgboxclass).classList.add('loaded');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><title></title>
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
*
|
|
||||||
* This file is part of a Moko Consulting project.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* FILE INFORMATION
|
|
||||||
* DEFGROUP: MokoOnyx.Override
|
|
||||||
* INGROUP: MokoOnyx
|
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
|
||||||
* PATH: /html/com_joomgallery/gallery/default.php
|
|
||||||
<<<<<<< HEAD
|
|
||||||
<<<<<<< HEAD
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
>>>>>>> origin/main
|
|
||||||
>>>>>>> origin/main
|
|
||||||
* BRIEF: Gallery view override — main image grid with masonry/justified layout
|
|
||||||
*/
|
|
||||||
|
|
||||||
// No direct access
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Router\Route;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\Layout\LayoutHelper;
|
|
||||||
|
|
||||||
// Image params
|
|
||||||
$image_type = $this->params['configs']->get('jg_gallery_view_type_image', 'thumbnail', 'STRING');
|
|
||||||
$gallery_class = $this->params['configs']->get('jg_gallery_view_class', 'masonry', 'STRING');
|
|
||||||
$num_columns = $this->params['configs']->get('jg_gallery_view_num_columns', 3, 'INT');
|
|
||||||
$image_class = $this->params['configs']->get('jg_gallery_view_image_class', 0, 'INT');
|
|
||||||
$justified_height = $this->params['configs']->get('jg_gallery_view_justified_height', 200, 'INT');
|
|
||||||
$justified_gap = $this->params['configs']->get('jg_gallery_view_justified_gap', 5, 'INT');
|
|
||||||
$image_link = $this->params['configs']->get('jg_gallery_view_image_link', 'defaultview', 'STRING');
|
|
||||||
$lightbox_image = $this->params['configs']->get('jg_category_view_lightbox_image', 'detail', 'STRING');
|
|
||||||
$browse_categories_link = $this->params['configs']->get('jg_gallery_view_browse_categories_link', 1, 'INT');
|
|
||||||
|
|
||||||
// Import CSS & JS
|
|
||||||
$wa = $this->document->getWebAssetManager();
|
|
||||||
$wa->useStyle('com_joomgallery.site');
|
|
||||||
$wa->useStyle('com_joomgallery.jg-icon-font');
|
|
||||||
|
|
||||||
if ($gallery_class == 'masonry') {
|
|
||||||
$wa->useScript('com_joomgallery.masonry');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($gallery_class == 'justified') {
|
|
||||||
$wa->useScript('com_joomgallery.justified');
|
|
||||||
$wa->addInlineStyle('.jg-images[class*=" justified-"] .jg-image-caption-hover { right: ' . $justified_gap . 'px; }');
|
|
||||||
}
|
|
||||||
|
|
||||||
$lightbox = false;
|
|
||||||
if ($image_link == 'lightgallery') {
|
|
||||||
$lightbox = true;
|
|
||||||
$wa->useScript('com_joomgallery.lightgallery');
|
|
||||||
$wa->useScript('com_joomgallery.lg-thumbnail');
|
|
||||||
$wa->useStyle('com_joomgallery.lightgallery-bundle');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialise the grid script
|
|
||||||
$iniJS = 'window.joomGrid = {';
|
|
||||||
$iniJS .= ' itemid: ' . $this->item->id . ',';
|
|
||||||
$iniJS .= ' pagination: 0,';
|
|
||||||
$iniJS .= ' layout: "' . $gallery_class . '",';
|
|
||||||
$iniJS .= ' num_columns: ' . $num_columns . ',';
|
|
||||||
$iniJS .= ' lightbox: ' . ($lightbox ? 'true' : 'false') . ',';
|
|
||||||
$iniJS .= ' justified: {height: ' . $justified_height . ', gap: ' . $justified_gap . '}';
|
|
||||||
$iniJS .= '};';
|
|
||||||
|
|
||||||
$wa->addInlineScript($iniJS, ['position' => 'before'], [], ['com_joomgallery.joomgrid']);
|
|
||||||
$wa->useScript('com_joomgallery.joomgrid');
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="com-joomgallery-gallery" itemscope itemtype="https://schema.org/ImageGallery">
|
|
||||||
<?php if ($this->params['menu']->get('show_page_heading')) : ?>
|
|
||||||
<div class="page-header">
|
|
||||||
<h1><?php echo $this->escape($this->params['menu']->get('page_heading')); ?></h1>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php // Browse categories link (top) ?>
|
|
||||||
<?php if ($browse_categories_link == '1') : ?>
|
|
||||||
<div class="text-center mb-4">
|
|
||||||
<a class="btn btn-outline-primary" href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=1'); ?>">
|
|
||||||
<i class="jg-icon-folder me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_GALLERY_VIEW_BROWSE_CATEGORIES'); ?>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (count($this->item->images->items) == 0) : ?>
|
|
||||||
<div class="alert alert-info" role="alert">
|
|
||||||
<p class="mb-0"><?php echo Text::_('COM_JOOMGALLERY_GALLERY_NO_IMAGES'); ?></p>
|
|
||||||
</div>
|
|
||||||
<?php else : ?>
|
|
||||||
<?php
|
|
||||||
$imgsData = [
|
|
||||||
'id' => (int) $this->item->id,
|
|
||||||
'layout' => $gallery_class,
|
|
||||||
'items' => $this->item->images->items,
|
|
||||||
'num_columns' => (int) $num_columns,
|
|
||||||
'caption_align' => 'center',
|
|
||||||
'image_class' => $image_class,
|
|
||||||
'image_type' => $image_type,
|
|
||||||
'lightbox_type' => $lightbox_image,
|
|
||||||
'image_link' => $image_link,
|
|
||||||
'image_title' => false,
|
|
||||||
'title_link' => 'defaultview',
|
|
||||||
'image_desc' => false,
|
|
||||||
'image_date' => false,
|
|
||||||
'image_author' => false,
|
|
||||||
'image_tags' => false,
|
|
||||||
];
|
|
||||||
?>
|
|
||||||
<?php echo LayoutHelper::render('joomgallery.grids.images', $imgsData); ?>
|
|
||||||
|
|
||||||
<?php // Pagination ?>
|
|
||||||
<nav class="mt-4" aria-label="<?php echo Text::_('JLIB_HTML_PAGINATION'); ?>">
|
|
||||||
<?php echo $this->item->images->pagination->getListFooter(); ?>
|
|
||||||
</nav>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php // Browse categories link (bottom) ?>
|
|
||||||
<?php if ($browse_categories_link == '2') : ?>
|
|
||||||
<div class="text-center mt-4">
|
|
||||||
<a class="btn btn-outline-primary" href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=1'); ?>">
|
|
||||||
<i class="jg-icon-folder me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_GALLERY_VIEW_BROWSE_CATEGORIES'); ?>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
if (window.joomGrid.layout != 'justified') {
|
|
||||||
document.querySelectorAll('.' + window.joomGrid.imgclass).forEach(function(image) {
|
|
||||||
image.addEventListener('load', function() {
|
|
||||||
this.closest('.' + window.joomGrid.imgboxclass).classList.add('loaded');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><title></title>
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
*
|
|
||||||
* This file is part of a Moko Consulting project.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* FILE INFORMATION
|
|
||||||
* DEFGROUP: MokoOnyx.Override
|
|
||||||
* INGROUP: MokoOnyx
|
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
|
||||||
* PATH: /html/com_joomgallery/image/default.php
|
|
||||||
<<<<<<< HEAD
|
|
||||||
<<<<<<< HEAD
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
>>>>>>> origin/main
|
|
||||||
>>>>>>> origin/main
|
|
||||||
* BRIEF: Image detail view override — single image with metadata, tags, custom fields
|
|
||||||
*/
|
|
||||||
|
|
||||||
// No direct access
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Router\Route;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\Session\Session;
|
|
||||||
use Joomla\CMS\HTML\HTMLHelper;
|
|
||||||
use Joomla\CMS\Layout\FileLayout;
|
|
||||||
use Joomla\CMS\User\UserFactoryInterface;
|
|
||||||
use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;
|
|
||||||
use Joomgallery\Component\Joomgallery\Administrator\Helper\JoomHelper;
|
|
||||||
|
|
||||||
// Image params
|
|
||||||
$image_type = $this->params['configs']->get('jg_detail_view_type_image', 'detail', 'STRING');
|
|
||||||
$show_title = $this->params['configs']->get('jg_detail_view_show_title', 0, 'INT');
|
|
||||||
$show_category = $this->params['configs']->get('jg_detail_view_show_category', 0, 'INT');
|
|
||||||
$show_description = $this->params['configs']->get('jg_detail_view_show_description', 0, 'INT');
|
|
||||||
$show_imgdate = $this->params['configs']->get('jg_detail_view_show_imgdate', 0, 'INT');
|
|
||||||
$show_imgauthor = $this->params['configs']->get('jg_detail_view_show_imgauthor', 0, 'INT');
|
|
||||||
$show_created_by = $this->params['configs']->get('jg_detail_view_show_created_by', 0, 'INT');
|
|
||||||
$show_votes = $this->params['configs']->get('jg_detail_view_show_votes', 0, 'INT');
|
|
||||||
$show_rating = $this->params['configs']->get('jg_detail_view_show_rating', 0, 'INT');
|
|
||||||
$show_hits = $this->params['configs']->get('jg_detail_view_show_hits', 0, 'INT');
|
|
||||||
$show_downloads = $this->params['configs']->get('jg_detail_view_show_downloads', 0, 'INT');
|
|
||||||
$show_tags = $this->params['configs']->get('jg_detail_view_show_tags', 0, 'INT');
|
|
||||||
$show_metadata = $this->params['configs']->get('jg_detail_view_show_metadata', 0, 'INT');
|
|
||||||
|
|
||||||
// Import CSS & JS
|
|
||||||
$wa = $this->document->getWebAssetManager();
|
|
||||||
$wa->useStyle('com_joomgallery.site');
|
|
||||||
$wa->useStyle('com_joomgallery.jg-icon-font');
|
|
||||||
|
|
||||||
// Access check
|
|
||||||
$canEdit = $this->getAcl()->checkACL('edit', 'com_joomgallery.image', $this->item->id, $this->item->catid, true);
|
|
||||||
$canDelete = $this->getAcl()->checkACL('delete', 'com_joomgallery.image', $this->item->id, $this->item->catid, true);
|
|
||||||
$canCheckin = $this->getAcl()->checkACL('editstate', 'com_joomgallery.image', $this->item->id, $this->item->catid, true) || $this->item->checked_out == Factory::getUser()->id;
|
|
||||||
$returnURL = base64_encode(JoomHelper::getViewRoute('image', $this->item->id, $this->item->catid, $this->item->language, $this->getLayout()));
|
|
||||||
|
|
||||||
// Tags
|
|
||||||
$tagLayout = new FileLayout('joomgallery.content.tags');
|
|
||||||
$tags = $tagLayout->render($this->item->tags);
|
|
||||||
|
|
||||||
// Metadata
|
|
||||||
$metadataLayout = new FileLayout('joomgallery.content.metadata');
|
|
||||||
$metadata = $metadataLayout->render($this->item->imgmetadata);
|
|
||||||
|
|
||||||
// Custom Fields
|
|
||||||
$fields = FieldsHelper::getFields('com_joomgallery.image', $this->item);
|
|
||||||
|
|
||||||
// Check if we have any info rows to show
|
|
||||||
$hasInfo = $show_category || $show_imgdate || $show_imgauthor || $show_created_by
|
|
||||||
|| $show_votes || $show_rating || $show_hits || $show_downloads
|
|
||||||
|| $show_tags || $show_metadata || count($fields) > 0;
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="com-joomgallery-image" itemscope itemtype="https://schema.org/ImageObject">
|
|
||||||
<?php if ($show_title) : ?>
|
|
||||||
<h2 itemprop="name"><?php echo $this->escape($this->item->title); ?></h2>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php // Back to category ?>
|
|
||||||
<a class="btn btn-outline-primary btn-sm mb-3" href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=' . (int) $this->item->catid); ?>">
|
|
||||||
<i class="jg-icon-arrow-left-alt me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_IMAGE_BACK_TO_CATEGORY') . ' ' . $this->escape($this->item->cattitle); ?>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<?php // Admin buttons ?>
|
|
||||||
<?php if ($canEdit || $canCheckin || $canDelete) : ?>
|
|
||||||
<div class="com-joomgallery-image__actions btn-toolbar mb-3" role="toolbar" aria-label="<?php echo Text::_('JTOOLBAR'); ?>">
|
|
||||||
<?php if ($canCheckin && $this->item->checked_out > 0) : ?>
|
|
||||||
<a class="btn btn-outline-secondary btn-sm me-2" href="<?php echo Route::_('index.php?option=com_joomgallery&task=image.checkin&id=' . $this->item->id . '&return=' . $returnURL . '&' . Session::getFormToken() . '=1'); ?>">
|
|
||||||
<i class="jg-icon-checkin me-1" aria-hidden="true"></i><?php echo Text::_('JLIB_HTML_CHECKIN'); ?>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($canEdit) : ?>
|
|
||||||
<a class="btn btn-outline-primary btn-sm me-2<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="<?php echo Route::_('index.php?option=com_joomgallery&task=image.edit&id=' . $this->item->id . '&return=' . $returnURL); ?>">
|
|
||||||
<i class="jg-icon-edit me-1" aria-hidden="true"></i><?php echo Text::_('JGLOBAL_EDIT'); ?>
|
|
||||||
</a>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($canDelete) : ?>
|
|
||||||
<a class="btn btn-outline-danger btn-sm<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="#deleteImgModal" role="button" data-bs-toggle="modal">
|
|
||||||
<i class="jg-icon-delete me-1" aria-hidden="true"></i><?php echo Text::_('JACTION_DELETE'); ?>
|
|
||||||
</a>
|
|
||||||
<?php echo HTMLHelper::_(
|
|
||||||
'bootstrap.renderModal',
|
|
||||||
'deleteImgModal',
|
|
||||||
[
|
|
||||||
'title' => Text::_('JACTION_DELETE'),
|
|
||||||
'modalWidth' => '50',
|
|
||||||
'bodyHeight' => '100',
|
|
||||||
'footer' => '<button class="btn btn-secondary" data-bs-dismiss="modal">' . Text::_('JCANCEL') . '</button>'
|
|
||||||
. '<a href="' . Route::_('index.php?option=com_joomgallery&task=image.remove&id=' . $this->item->id . '&return=' . $returnURL . '&' . Session::getFormToken() . '=1', false, 2) . '" class="btn btn-danger">' . Text::_('COM_JOOMGALLERY_COMMON_DELETE_IMAGE_TIPCAPTION') . '</a>',
|
|
||||||
],
|
|
||||||
Text::_('COM_JOOMGALLERY_COMMON_ALERT_SURE_DELETE_SELECTED_ITEM')
|
|
||||||
); ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php // Image ?>
|
|
||||||
<figure class="figure com-joomgallery-image__figure text-center w-100 mb-4">
|
|
||||||
<div id="jg-loader"></div>
|
|
||||||
<img
|
|
||||||
src="<?php echo JoomHelper::getImg($this->item, $image_type); ?>"
|
|
||||||
class="figure-img img-fluid rounded"
|
|
||||||
alt="<?php echo $this->escape($this->item->title); ?>"
|
|
||||||
style="width:auto;"
|
|
||||||
itemprop="contentUrl"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<?php if ($show_description && !empty($this->item->description)) : ?>
|
|
||||||
<figcaption class="figure-caption" itemprop="description"><?php echo $this->item->description; ?></figcaption>
|
|
||||||
<?php endif; ?>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<?php // Image info table ?>
|
|
||||||
<?php if ($hasInfo) : ?>
|
|
||||||
<div class="com-joomgallery-image__info">
|
|
||||||
<h3><?php echo Text::_('COM_JOOMGALLERY_IMAGE_INFO'); ?></h3>
|
|
||||||
<table class="table table-striped">
|
|
||||||
<tbody>
|
|
||||||
<?php if ($show_category) : ?>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('JCATEGORY'); ?></th>
|
|
||||||
<td>
|
|
||||||
<a href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=' . (int) $this->item->catid); ?>">
|
|
||||||
<?php echo $this->escape($this->item->cattitle); ?>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($show_imgdate) : ?>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_DATE'); ?></th>
|
|
||||||
<td>
|
|
||||||
<time datetime="<?php echo HTMLHelper::_('date', $this->item->date, 'c'); ?>" itemprop="dateCreated">
|
|
||||||
<?php echo HTMLHelper::_('date', $this->item->date, Text::_('DATE_FORMAT_LC4')); ?>
|
|
||||||
</time>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($show_imgauthor) : ?>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('JAUTHOR'); ?></th>
|
|
||||||
<td itemprop="author"><?php echo $this->escape($this->item->author); ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($show_created_by) : ?>
|
|
||||||
<?php $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($this->item->created_by); ?>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_OWNER'); ?></th>
|
|
||||||
<td><?php echo $this->escape($user->name); ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($show_votes) : ?>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_VOTES'); ?></th>
|
|
||||||
<td><?php echo $this->escape($this->item->votes); ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($show_rating) : ?>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_IMAGE_RATING'); ?></th>
|
|
||||||
<td><?php echo $this->escape($this->item->rating); ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($show_hits) : ?>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('JGLOBAL_HITS'); ?></th>
|
|
||||||
<td><?php echo (int) $this->item->hits; ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($show_downloads) : ?>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_DOWNLOADS'); ?></th>
|
|
||||||
<td><?php echo (int) $this->item->downloads; ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($show_tags) : ?>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_TAGS'); ?></th>
|
|
||||||
<td><?php echo $tags; ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($show_metadata) : ?>
|
|
||||||
<tr>
|
|
||||||
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_IMGMETADATA'); ?></th>
|
|
||||||
<td><?php echo $metadata; ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php // Custom fields ?>
|
|
||||||
<?php if (count($fields) > 0) : ?>
|
|
||||||
<tr>
|
|
||||||
<th scope="row" colspan="2"><strong><?php echo Text::_('JGLOBAL_FIELDS'); ?></strong></th>
|
|
||||||
</tr>
|
|
||||||
<?php foreach ($fields as $field) : ?>
|
|
||||||
<?php if ($this->component->getAccess()->checkViewLevel($field->access) && $field->params->get('display') > 0) : ?>
|
|
||||||
<tr class="<?php echo $this->escape($field->params->get('render_class')); ?>">
|
|
||||||
<?php if ($field->params->get('showlabel', true)) : ?>
|
|
||||||
<th scope="row" class="<?php echo $this->escape($field->params->get('label_render_class')); ?>"><?php echo $this->escape($field->title); ?></th>
|
|
||||||
<?php else : ?>
|
|
||||||
<th scope="row"></th>
|
|
||||||
<?php endif; ?>
|
|
||||||
<td class="<?php echo $this->escape($field->params->get('value_render_class')); ?>"><?php echo $field->value; ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.onload = function() {
|
|
||||||
var el = document.querySelector('#jg-loader');
|
|
||||||
if (el) el.classList.add('hidden');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!DOCTYPE html><title></title>
|
|
||||||
@@ -10,17 +10,7 @@
|
|||||||
* INGROUP: MokoOnyx
|
* INGROUP: MokoOnyx
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
* PATH: /html/layouts/joomla/module/card.php
|
* PATH: /html/layouts/joomla/module/card.php
|
||||||
<<<<<<< HEAD
|
* VERSION: 02.21.05
|
||||||
<<<<<<< HEAD
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
>>>>>>> origin/main
|
|
||||||
>>>>>>> origin/main
|
|
||||||
* BRIEF: Custom card module chrome — renders module titles for all modules
|
* BRIEF: Custom card module chrome — renders module titles for all modules
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -11,17 +11,7 @@
|
|||||||
* INGROUP: MokoOnyx.Layouts
|
* INGROUP: MokoOnyx.Layouts
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
* PATH: /src/html/layouts/mokoonyx/article-metadata.php
|
* PATH: /src/html/layouts/mokoonyx/article-metadata.php
|
||||||
<<<<<<< HEAD
|
* VERSION: 02.21.05
|
||||||
<<<<<<< HEAD
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
>>>>>>> origin/main
|
|
||||||
>>>>>>> origin/main
|
|
||||||
* BRIEF: Article metadata footer layout -- renders jcfields grouped by field group
|
* BRIEF: Article metadata footer layout -- renders jcfields grouped by field group
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
|
||||||
|
This file is part of a Moko Consulting project.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Social icons layout — rendered by index.php in topbar and/or footer.
|
||||||
|
*
|
||||||
|
* Expected $displayData keys:
|
||||||
|
* 'params' => Joomla\Registry\Registry (template params)
|
||||||
|
* 'position' => string ('topbar' | 'footer' | 'floating')
|
||||||
|
*/
|
||||||
|
|
||||||
|
$params = $displayData['params'];
|
||||||
|
$position = $displayData['position'] ?? 'footer';
|
||||||
|
|
||||||
|
// Platform definitions: key => [FA icon class, language key for aria-label]
|
||||||
|
$platforms = [
|
||||||
|
'facebook' => ['fa-brands fa-facebook-f', 'TPL_MOKOONYX_SOCIAL_FACEBOOK'],
|
||||||
|
'twitter' => ['fa-brands fa-x-twitter', 'TPL_MOKOONYX_SOCIAL_TWITTER'],
|
||||||
|
'instagram' => ['fa-brands fa-instagram', 'TPL_MOKOONYX_SOCIAL_INSTAGRAM'],
|
||||||
|
'linkedin' => ['fa-brands fa-linkedin-in', 'TPL_MOKOONYX_SOCIAL_LINKEDIN'],
|
||||||
|
'youtube' => ['fa-brands fa-youtube', 'TPL_MOKOONYX_SOCIAL_YOUTUBE'],
|
||||||
|
'github' => ['fa-brands fa-github', 'TPL_MOKOONYX_SOCIAL_GITHUB'],
|
||||||
|
'bluesky' => ['fa-brands fa-bluesky', 'TPL_MOKOONYX_SOCIAL_BLUESKY'],
|
||||||
|
'threads' => ['fa-brands fa-threads', 'TPL_MOKOONYX_SOCIAL_THREADS'],
|
||||||
|
'discord' => ['fa-brands fa-discord', 'TPL_MOKOONYX_SOCIAL_DISCORD'],
|
||||||
|
'tiktok' => ['fa-brands fa-tiktok', 'TPL_MOKOONYX_SOCIAL_TIKTOK'],
|
||||||
|
'reddit' => ['fa-brands fa-reddit-alien', 'TPL_MOKOONYX_SOCIAL_REDDIT'],
|
||||||
|
'pinterest' => ['fa-brands fa-pinterest-p', 'TPL_MOKOONYX_SOCIAL_PINTEREST'],
|
||||||
|
'snapchat' => ['fa-brands fa-snapchat', 'TPL_MOKOONYX_SOCIAL_SNAPCHAT'],
|
||||||
|
'telegram' => ['fa-brands fa-telegram', 'TPL_MOKOONYX_SOCIAL_TELEGRAM'],
|
||||||
|
'whatsapp' => ['fa-brands fa-whatsapp', 'TPL_MOKOONYX_SOCIAL_WHATSAPP'],
|
||||||
|
'tumblr' => ['fa-brands fa-tumblr', 'TPL_MOKOONYX_SOCIAL_TUMBLR'],
|
||||||
|
'twitch' => ['fa-brands fa-twitch', 'TPL_MOKOONYX_SOCIAL_TWITCH'],
|
||||||
|
'spotify' => ['fa-brands fa-spotify', 'TPL_MOKOONYX_SOCIAL_SPOTIFY'],
|
||||||
|
'soundcloud' => ['fa-brands fa-soundcloud', 'TPL_MOKOONYX_SOCIAL_SOUNDCLOUD'],
|
||||||
|
'flickr' => ['fa-brands fa-flickr', 'TPL_MOKOONYX_SOCIAL_FLICKR'],
|
||||||
|
'vimeo' => ['fa-brands fa-vimeo-v', 'TPL_MOKOONYX_SOCIAL_VIMEO'],
|
||||||
|
'linktree' => ['fa-solid fa-link', 'TPL_MOKOONYX_SOCIAL_LINKTREE'],
|
||||||
|
'mail' => ['fa-solid fa-envelope', 'TPL_MOKOONYX_SOCIAL_MAIL'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Collect enabled platforms (those with a non-empty URL)
|
||||||
|
$active = [];
|
||||||
|
foreach ($platforms as $key => [$iconClass, $langKey]) {
|
||||||
|
$url = trim((string) $params->get('social_' . $key . '_url', ''));
|
||||||
|
if ($url !== '' && preg_match('#^(https?://|mailto:|/)#i', $url)) {
|
||||||
|
$active[] = [
|
||||||
|
'url' => $url,
|
||||||
|
'iconClass' => $iconClass,
|
||||||
|
'label' => Text::_($langKey),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($active)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$style = in_array($params->get('social_icon_style'), ['plain', 'square', 'circle', 'rounded'], true)
|
||||||
|
? $params->get('social_icon_style') : 'plain';
|
||||||
|
$size = in_array($params->get('social_icon_size'), ['sm', 'md', 'lg'], true)
|
||||||
|
? $params->get('social_icon_size') : 'md';
|
||||||
|
|
||||||
|
$floatingPos = in_array($params->get('social_floating_pos'), ['left', 'right'], true)
|
||||||
|
? $params->get('social_floating_pos') : 'left';
|
||||||
|
$colorMode = in_array($params->get('social_icon_color'), ['theme', 'brand', 'black', 'white'], true)
|
||||||
|
? $params->get('social_icon_color') : 'theme';
|
||||||
|
|
||||||
|
$listClass = 'moko-social-icons';
|
||||||
|
$listClass .= ' moko-social-icons--' . htmlspecialchars($style, ENT_QUOTES, 'UTF-8');
|
||||||
|
$listClass .= ' moko-social-icons--' . htmlspecialchars($size, ENT_QUOTES, 'UTF-8');
|
||||||
|
$listClass .= ' moko-social-icons--' . htmlspecialchars($position, ENT_QUOTES, 'UTF-8');
|
||||||
|
$listClass .= ' moko-social-icons--color-' . $colorMode;
|
||||||
|
if ($position === 'floating') {
|
||||||
|
$listClass .= ' moko-social-icons--floating-' . $floatingPos;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<nav class="<?php echo $listClass; ?>" aria-label="<?php echo Text::_('TPL_MOKOONYX_SOCIAL_NAV_LABEL'); ?>">
|
||||||
|
<ul>
|
||||||
|
<?php foreach ($active as $item) : ?>
|
||||||
|
<li>
|
||||||
|
<a href="<?php echo htmlspecialchars($item['url'], ENT_QUOTES, 'UTF-8'); ?>"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="<?php echo htmlspecialchars($item['label'], ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
|
<span class="<?php echo htmlspecialchars($item['iconClass'], ENT_QUOTES, 'UTF-8'); ?>" aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
@@ -33,6 +33,8 @@ if ($item->anchor_rel) {
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
|
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||||
|
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ $anchor_css = $item->anchor_css ?: '';
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
|
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||||
|
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ $anchor_css = $item->anchor_css ?: '';
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
|
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||||
|
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ if ($item->anchor_rel) {
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
|
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||||
|
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ if ($item->anchor_rel) {
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
|
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||||
|
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ $anchor_css = $item->anchor_css ?: '';
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
|
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||||
|
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ $anchor_css = $item->anchor_css ?: '';
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
|
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||||
|
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ if ($item->anchor_rel) {
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
|
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||||
|
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ if ($item->anchor_rel) {
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
|
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||||
|
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||||
// The link is an icon
|
// The link is an icon
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
// If the link text is to be displayed, the icon is added with aria-hidden
|
// If the link text is to be displayed, the icon is added with aria-hidden
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ $anchor_css = $item->anchor_css ?: '';
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
|
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||||
|
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||||
// The link is an icon
|
// The link is an icon
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
// If the link text is to be displayed, the icon is added with aria-hidden
|
// If the link text is to be displayed, the icon is added with aria-hidden
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ $anchor_css = $item->anchor_css ?: '';
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
|
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||||
|
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||||
// The link is an icon
|
// The link is an icon
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
// If the link text is to be displayed, the icon is added with aria-hidden
|
// If the link text is to be displayed, the icon is added with aria-hidden
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ if ($item->anchor_rel) {
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
|
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
||||||
|
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
||||||
// The link is an icon
|
// The link is an icon
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
// If the link text is to be displayed, the icon is added with aria-hidden
|
// If the link text is to be displayed, the icon is added with aria-hidden
|
||||||
|
|||||||
+13
-1
@@ -436,9 +436,12 @@ $wa->useScript('user.js'); // js/user.js
|
|||||||
<?php if (!$hideHeaderHome) : ?>
|
<?php if (!$hideHeaderHome) : ?>
|
||||||
<header id="top" class="header container-header full-width<?php echo $stickyHeader ? ' ' . $stickyHeader : ''; ?>" role="banner">
|
<header id="top" class="header container-header full-width<?php echo $stickyHeader ? ' ' . $stickyHeader : ''; ?>" role="banner">
|
||||||
|
|
||||||
<?php if ($this->countModules('topbar')) : ?>
|
<?php if ($this->countModules('topbar') || $this->params->get('social_topbar')) : ?>
|
||||||
<div class="container-topbar">
|
<div class="container-topbar">
|
||||||
<jdoc:include type="modules" name="topbar" style="none" />
|
<jdoc:include type="modules" name="topbar" style="none" />
|
||||||
|
<?php if ($this->params->get('social_topbar')) :
|
||||||
|
echo \Joomla\CMS\Layout\LayoutHelper::render('mokoonyx.social-icons', ['params' => $this->params, 'position' => 'topbar']);
|
||||||
|
endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
@@ -590,6 +593,11 @@ $wa->useScript('user.js'); // js/user.js
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="container-footer footer full-width">
|
<footer class="container-footer footer full-width">
|
||||||
|
<?php if ($this->params->get('social_footer')) : ?>
|
||||||
|
<div class="grid-child footer-social">
|
||||||
|
<?php echo \Joomla\CMS\Layout\LayoutHelper::render('mokoonyx.social-icons', ['params' => $this->params, 'position' => 'footer']); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if ($this->countModules('footer-menu', true)) : ?>
|
<?php if ($this->countModules('footer-menu', true)) : ?>
|
||||||
<div class="grid-child footer-menu">
|
<div class="grid-child footer-menu">
|
||||||
<jdoc:include type="modules" name="footer-menu" />
|
<jdoc:include type="modules" name="footer-menu" />
|
||||||
@@ -602,6 +610,10 @@ $wa->useScript('user.js'); // js/user.js
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<?php if ($this->params->get('social_floating')) :
|
||||||
|
echo \Joomla\CMS\Layout\LayoutHelper::render('mokoonyx.social-icons', ['params' => $this->params, 'position' => 'floating']);
|
||||||
|
endif; ?>
|
||||||
|
|
||||||
<?php if ($this->params->get('backTop') == 1) : ?>
|
<?php if ($this->params->get('backTop') == 1) : ?>
|
||||||
<a href="#top" id="back-top" class="back-to-top-link" aria-label="<?php echo Text::_('TPL_MOKOONYX_BACKTOTOP'); ?>">
|
<a href="#top" id="back-top" class="back-to-top-link" aria-label="<?php echo Text::_('TPL_MOKOONYX_BACKTOTOP'); ?>">
|
||||||
<span class="fa-solid fa-arrow-up" aria-hidden="true"></span>
|
<span class="fa-solid fa-arrow-up" aria-hidden="true"></span>
|
||||||
|
|||||||
@@ -262,6 +262,67 @@ TPL_MOKOONYX_CSS_VARS_GABLE_DESC="Colour tokens used by the Gable extension.<br>
|
|||||||
TPL_MOKOONYX_CSS_VARS_FOOTER_LABEL="Footer"
|
TPL_MOKOONYX_CSS_VARS_FOOTER_LABEL="Footer"
|
||||||
TPL_MOKOONYX_CSS_VARS_FOOTER_DESC="<strong>Spacing</strong><br><code>--footer-padding-top</code> — Top padding (default: <code>1rem</code>)<br><code>--footer-padding-bottom</code> — Bottom padding (default: <code>80px</code>)<br><code>--footer-grid-padding-y</code> — Grid vertical padding (default: <code>2.5rem</code>)<br><code>--footer-grid-padding-x</code> — Grid horizontal padding (default: <code>0.5em</code>)"
|
TPL_MOKOONYX_CSS_VARS_FOOTER_DESC="<strong>Spacing</strong><br><code>--footer-padding-top</code> — Top padding (default: <code>1rem</code>)<br><code>--footer-padding-bottom</code> — Bottom padding (default: <code>80px</code>)<br><code>--footer-grid-padding-y</code> — Grid vertical padding (default: <code>2.5rem</code>)<br><code>--footer-grid-padding-x</code> — Grid horizontal padding (default: <code>0.5em</code>)"
|
||||||
|
|
||||||
|
; ===== Social Icons =====
|
||||||
|
TPL_MOKOONYX_SOCIAL_FIELDSET_LABEL="Social Icons"
|
||||||
|
TPL_MOKOONYX_SOCIAL_NOTE="<p>Add your social media profile URLs below. Only platforms with a URL will be displayed. Icons use Font Awesome 7 and inherit your template's theme colours.</p>"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TOPBAR_LABEL="Show in Topbar"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TOPBAR_DESC="Display social icons in the topbar area (right-aligned on desktop, centred on mobile)."
|
||||||
|
TPL_MOKOONYX_SOCIAL_FOOTER_LABEL="Show in Footer"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FOOTER_DESC="Display social icons centred above the footer content."
|
||||||
|
TPL_MOKOONYX_SOCIAL_STYLE_LABEL="Icon Style"
|
||||||
|
TPL_MOKOONYX_SOCIAL_STYLE_DESC="Choose the visual style for social icons."
|
||||||
|
TPL_MOKOONYX_SOCIAL_STYLE_PLAIN="Plain (no background)"
|
||||||
|
TPL_MOKOONYX_SOCIAL_STYLE_SQUARE="Square background"
|
||||||
|
TPL_MOKOONYX_SOCIAL_STYLE_CIRCLE="Circle background"
|
||||||
|
TPL_MOKOONYX_SOCIAL_STYLE_ROUNDED="Rounded background"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SIZE_LABEL="Icon Size"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SIZE_DESC="Choose the size of social icons."
|
||||||
|
TPL_MOKOONYX_SOCIAL_SIZE_SM="Small"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SIZE_MD="Medium"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SIZE_LG="Large"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLOATING_LABEL="Show Floating Sidebar"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLOATING_DESC="Display social icons as a fixed vertical bar on the side of the viewport. Hidden on mobile devices."
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLOATING_POS_LABEL="Floating Position"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLOATING_POS_DESC="Which side of the screen the floating social bar appears on."
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLOATING_POS_LEFT="Left"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLOATING_POS_RIGHT="Right"
|
||||||
|
TPL_MOKOONYX_SOCIAL_COLOR_LABEL="Icon Colour"
|
||||||
|
TPL_MOKOONYX_SOCIAL_COLOR_DESC="Choose the colour scheme for social icons."
|
||||||
|
TPL_MOKOONYX_SOCIAL_COLOR_THEME="Theme (CSS variables)"
|
||||||
|
TPL_MOKOONYX_SOCIAL_COLOR_BRAND="Brand colours"
|
||||||
|
TPL_MOKOONYX_SOCIAL_COLOR_BLACK="Black"
|
||||||
|
TPL_MOKOONYX_SOCIAL_COLOR_WHITE="White"
|
||||||
|
TPL_MOKOONYX_SOCIAL_URLS_NOTE="<p>Enter the full URL for each platform. Leave blank to hide that icon.</p>"
|
||||||
|
TPL_MOKOONYX_SOCIAL_MAIL_DESC="Use a mailto: link (e.g. mailto:hello@example.com) or a contact page URL."
|
||||||
|
TPL_MOKOONYX_SOCIAL_NAV_LABEL="Social media links"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FACEBOOK="Follow us on Facebook"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TWITTER="Follow us on X"
|
||||||
|
TPL_MOKOONYX_SOCIAL_INSTAGRAM="Follow us on Instagram"
|
||||||
|
TPL_MOKOONYX_SOCIAL_LINKEDIN="Follow us on LinkedIn"
|
||||||
|
TPL_MOKOONYX_SOCIAL_YOUTUBE="Subscribe on YouTube"
|
||||||
|
TPL_MOKOONYX_SOCIAL_GITHUB="Follow us on GitHub"
|
||||||
|
TPL_MOKOONYX_SOCIAL_BLUESKY="Follow us on Bluesky"
|
||||||
|
TPL_MOKOONYX_SOCIAL_THREADS="Follow us on Threads"
|
||||||
|
TPL_MOKOONYX_SOCIAL_DISCORD="Join us on Discord"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TIKTOK="Follow us on TikTok"
|
||||||
|
TPL_MOKOONYX_SOCIAL_REDDIT="Join us on Reddit"
|
||||||
|
TPL_MOKOONYX_SOCIAL_PINTEREST="Follow us on Pinterest"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SNAPCHAT="Follow us on Snapchat"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TELEGRAM="Join us on Telegram"
|
||||||
|
TPL_MOKOONYX_SOCIAL_WHATSAPP="Chat on WhatsApp"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TUMBLR="Follow us on Tumblr"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TWITCH="Follow us on Twitch"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SPOTIFY="Listen on Spotify"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SOUNDCLOUD="Listen on SoundCloud"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLICKR="Follow us on Flickr"
|
||||||
|
TPL_MOKOONYX_SOCIAL_VIMEO="Follow us on Vimeo"
|
||||||
|
TPL_MOKOONYX_SOCIAL_LINKTREE="Visit our Linktree"
|
||||||
|
TPL_MOKOONYX_SOCIAL_MAIL="Email us"
|
||||||
|
|
||||||
|
; ===== CSS Variables tab (social) =====
|
||||||
|
TPL_MOKOONYX_CSS_VARS_SOCIAL_LABEL="Social Icons"
|
||||||
|
TPL_MOKOONYX_CSS_VARS_SOCIAL_DESC="<code>--social-icon-size</code> — Icon font size (overrides size preset)<br><code>--social-icon-gap</code> — Gap between icons (default: <code>0.5rem</code>)<br><code>--social-icon-color</code> — Icon colour (default: <code>currentColor</code>)<br><code>--social-icon-hover-color</code> — Hover colour (default: <code>var(--accent-color-primary)</code>)<br><code>--social-icon-bg</code> — Background for circle/rounded styles<br><code>--social-icon-hover-bg</code> — Hover background<br><code>--social-icon-radius</code> — Border radius for rounded style (default: <code>0.375rem</code>)"
|
||||||
|
|
||||||
; ===== Misc =====
|
; ===== Misc =====
|
||||||
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"
|
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"
|
||||||
|
|
||||||
|
|||||||
@@ -262,6 +262,67 @@ TPL_MOKOONYX_CSS_VARS_GABLE_DESC="Color tokens used by the Gable extension.<br><
|
|||||||
TPL_MOKOONYX_CSS_VARS_FOOTER_LABEL="Footer"
|
TPL_MOKOONYX_CSS_VARS_FOOTER_LABEL="Footer"
|
||||||
TPL_MOKOONYX_CSS_VARS_FOOTER_DESC="<strong>Spacing</strong><br><code>--footer-padding-top</code> — Top padding (default: <code>1rem</code>)<br><code>--footer-padding-bottom</code> — Bottom padding (default: <code>80px</code>)<br><code>--footer-grid-padding-y</code> — Grid vertical padding (default: <code>2.5rem</code>)<br><code>--footer-grid-padding-x</code> — Grid horizontal padding (default: <code>0.5em</code>)"
|
TPL_MOKOONYX_CSS_VARS_FOOTER_DESC="<strong>Spacing</strong><br><code>--footer-padding-top</code> — Top padding (default: <code>1rem</code>)<br><code>--footer-padding-bottom</code> — Bottom padding (default: <code>80px</code>)<br><code>--footer-grid-padding-y</code> — Grid vertical padding (default: <code>2.5rem</code>)<br><code>--footer-grid-padding-x</code> — Grid horizontal padding (default: <code>0.5em</code>)"
|
||||||
|
|
||||||
|
; ===== Social Icons =====
|
||||||
|
TPL_MOKOONYX_SOCIAL_FIELDSET_LABEL="Social Icons"
|
||||||
|
TPL_MOKOONYX_SOCIAL_NOTE="<p>Add your social media profile URLs below. Only platforms with a URL will be displayed. Icons use Font Awesome 7 and inherit your template's theme colors.</p>"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TOPBAR_LABEL="Show in Topbar"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TOPBAR_DESC="Display social icons in the topbar area (right-aligned on desktop, centered on mobile)."
|
||||||
|
TPL_MOKOONYX_SOCIAL_FOOTER_LABEL="Show in Footer"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FOOTER_DESC="Display social icons centered above the footer content."
|
||||||
|
TPL_MOKOONYX_SOCIAL_STYLE_LABEL="Icon Style"
|
||||||
|
TPL_MOKOONYX_SOCIAL_STYLE_DESC="Choose the visual style for social icons."
|
||||||
|
TPL_MOKOONYX_SOCIAL_STYLE_PLAIN="Plain (no background)"
|
||||||
|
TPL_MOKOONYX_SOCIAL_STYLE_SQUARE="Square background"
|
||||||
|
TPL_MOKOONYX_SOCIAL_STYLE_CIRCLE="Circle background"
|
||||||
|
TPL_MOKOONYX_SOCIAL_STYLE_ROUNDED="Rounded background"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SIZE_LABEL="Icon Size"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SIZE_DESC="Choose the size of social icons."
|
||||||
|
TPL_MOKOONYX_SOCIAL_SIZE_SM="Small"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SIZE_MD="Medium"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SIZE_LG="Large"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLOATING_LABEL="Show Floating Sidebar"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLOATING_DESC="Display social icons as a fixed vertical bar on the side of the viewport. Hidden on mobile devices."
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLOATING_POS_LABEL="Floating Position"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLOATING_POS_DESC="Which side of the screen the floating social bar appears on."
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLOATING_POS_LEFT="Left"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLOATING_POS_RIGHT="Right"
|
||||||
|
TPL_MOKOONYX_SOCIAL_COLOR_LABEL="Icon Color"
|
||||||
|
TPL_MOKOONYX_SOCIAL_COLOR_DESC="Choose the color scheme for social icons."
|
||||||
|
TPL_MOKOONYX_SOCIAL_COLOR_THEME="Theme (CSS variables)"
|
||||||
|
TPL_MOKOONYX_SOCIAL_COLOR_BRAND="Brand colors"
|
||||||
|
TPL_MOKOONYX_SOCIAL_COLOR_BLACK="Black"
|
||||||
|
TPL_MOKOONYX_SOCIAL_COLOR_WHITE="White"
|
||||||
|
TPL_MOKOONYX_SOCIAL_URLS_NOTE="<p>Enter the full URL for each platform. Leave blank to hide that icon.</p>"
|
||||||
|
TPL_MOKOONYX_SOCIAL_MAIL_DESC="Use a mailto: link (e.g. mailto:hello@example.com) or a contact page URL."
|
||||||
|
TPL_MOKOONYX_SOCIAL_NAV_LABEL="Social media links"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FACEBOOK="Follow us on Facebook"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TWITTER="Follow us on X"
|
||||||
|
TPL_MOKOONYX_SOCIAL_INSTAGRAM="Follow us on Instagram"
|
||||||
|
TPL_MOKOONYX_SOCIAL_LINKEDIN="Follow us on LinkedIn"
|
||||||
|
TPL_MOKOONYX_SOCIAL_YOUTUBE="Subscribe on YouTube"
|
||||||
|
TPL_MOKOONYX_SOCIAL_GITHUB="Follow us on GitHub"
|
||||||
|
TPL_MOKOONYX_SOCIAL_BLUESKY="Follow us on Bluesky"
|
||||||
|
TPL_MOKOONYX_SOCIAL_THREADS="Follow us on Threads"
|
||||||
|
TPL_MOKOONYX_SOCIAL_DISCORD="Join us on Discord"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TIKTOK="Follow us on TikTok"
|
||||||
|
TPL_MOKOONYX_SOCIAL_REDDIT="Join us on Reddit"
|
||||||
|
TPL_MOKOONYX_SOCIAL_PINTEREST="Follow us on Pinterest"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SNAPCHAT="Follow us on Snapchat"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TELEGRAM="Join us on Telegram"
|
||||||
|
TPL_MOKOONYX_SOCIAL_WHATSAPP="Chat on WhatsApp"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TUMBLR="Follow us on Tumblr"
|
||||||
|
TPL_MOKOONYX_SOCIAL_TWITCH="Follow us on Twitch"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SPOTIFY="Listen on Spotify"
|
||||||
|
TPL_MOKOONYX_SOCIAL_SOUNDCLOUD="Listen on SoundCloud"
|
||||||
|
TPL_MOKOONYX_SOCIAL_FLICKR="Follow us on Flickr"
|
||||||
|
TPL_MOKOONYX_SOCIAL_VIMEO="Follow us on Vimeo"
|
||||||
|
TPL_MOKOONYX_SOCIAL_LINKTREE="Visit our Linktree"
|
||||||
|
TPL_MOKOONYX_SOCIAL_MAIL="Email us"
|
||||||
|
|
||||||
|
; ===== CSS Variables tab (social) =====
|
||||||
|
TPL_MOKOONYX_CSS_VARS_SOCIAL_LABEL="Social Icons"
|
||||||
|
TPL_MOKOONYX_CSS_VARS_SOCIAL_DESC="<code>--social-icon-size</code> — Icon font size (overrides size preset)<br><code>--social-icon-gap</code> — Gap between icons (default: <code>0.5rem</code>)<br><code>--social-icon-color</code> — Icon color (default: <code>currentColor</code>)<br><code>--social-icon-hover-color</code> — Hover color (default: <code>var(--accent-color-primary)</code>)<br><code>--social-icon-bg</code> — Background for circle/rounded styles<br><code>--social-icon-hover-bg</code> — Hover background<br><code>--social-icon-radius</code> — Border radius for rounded style (default: <code>0.375rem</code>)"
|
||||||
|
|
||||||
; ===== Misc =====
|
; ===== Misc =====
|
||||||
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"
|
MOD_BREADCRUMBS_HERE="YOU ARE HERE:"
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,7 @@
|
|||||||
* INGROUP: MokoOnyx.Accessibility
|
* INGROUP: MokoOnyx.Accessibility
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
* PATH: ./media/css/a11y-high-contrast.css
|
* PATH: ./media/css/a11y-high-contrast.css
|
||||||
<<<<<<< HEAD
|
* VERSION: 02.21.05
|
||||||
<<<<<<< HEAD
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
=======
|
|
||||||
* VERSION: 02.15.02
|
|
||||||
>>>>>>> origin/main
|
|
||||||
>>>>>>> origin/main
|
|
||||||
* BRIEF: High-contrast stylesheet for accessibility toolbar
|
* BRIEF: High-contrast stylesheet for accessibility toolbar
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+191
-2
@@ -38,7 +38,7 @@
|
|||||||
nav,
|
nav,
|
||||||
.container-topbar,
|
.container-topbar,
|
||||||
.container-nav,
|
.container-nav,
|
||||||
#rssocial-133,
|
.moko-social-icons,
|
||||||
.container-sidebar-right,
|
.container-sidebar-right,
|
||||||
.container-sidebar-left,
|
.container-sidebar-left,
|
||||||
.container-bottom-a,
|
.container-bottom-a,
|
||||||
@@ -23528,4 +23528,193 @@ font-size: 0.8125rem;
|
|||||||
.fa-brands {
|
.fa-brands {
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
/* patch release test */
|
|
||||||
|
/* ===== Social Icons ===== */
|
||||||
|
.moko-social-icons ul {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
gap: var(--social-icon-gap, 0.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.moko-social-icons a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--social-icon-color, var(--accent-color-secondary, var(--accent-color-primary, currentColor)));
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s ease, background-color 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moko-social-icons a:hover,
|
||||||
|
.moko-social-icons a:focus-visible {
|
||||||
|
color: var(--social-icon-hover-color, var(--accent-color-primary, #3f8ff0));
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.moko-social-icons a .fa-brands,
|
||||||
|
.moko-social-icons a .fa-solid {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Size variants */
|
||||||
|
.moko-social-icons--sm a { font-size: var(--social-icon-size, 1rem); }
|
||||||
|
.moko-social-icons--md a { font-size: var(--social-icon-size, 1.25rem); }
|
||||||
|
.moko-social-icons--lg a { font-size: var(--social-icon-size, 1.75rem); }
|
||||||
|
|
||||||
|
/* Color mode: theme (default) — inherits from CSS variables */
|
||||||
|
.moko-social-icons--color-theme a {
|
||||||
|
color: var(--social-icon-color, var(--accent-color-secondary, var(--accent-color-primary, currentColor)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color mode: black */
|
||||||
|
.moko-social-icons--color-black a {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color mode: white */
|
||||||
|
.moko-social-icons--color-white a {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color mode: brand — official platform colours */
|
||||||
|
.moko-social-icons--color-brand .fa-facebook-f { color: #1877f2; }
|
||||||
|
.moko-social-icons--color-brand .fa-x-twitter { color: #000; }
|
||||||
|
.moko-social-icons--color-brand .fa-instagram { color: #e4405f; }
|
||||||
|
.moko-social-icons--color-brand .fa-linkedin-in { color: #0a66c2; }
|
||||||
|
.moko-social-icons--color-brand .fa-youtube { color: #ff0000; }
|
||||||
|
.moko-social-icons--color-brand .fa-github { color: #333; }
|
||||||
|
.moko-social-icons--color-brand .fa-bluesky { color: #0085ff; }
|
||||||
|
.moko-social-icons--color-brand .fa-threads { color: #000; }
|
||||||
|
.moko-social-icons--color-brand .fa-discord { color: #5865f2; }
|
||||||
|
.moko-social-icons--color-brand .fa-tiktok { color: #000; }
|
||||||
|
.moko-social-icons--color-brand .fa-reddit-alien { color: #ff4500; }
|
||||||
|
.moko-social-icons--color-brand .fa-pinterest-p { color: #bd081c; }
|
||||||
|
.moko-social-icons--color-brand .fa-snapchat { color: #fffc00; }
|
||||||
|
.moko-social-icons--color-brand .fa-telegram { color: #26a5e4; }
|
||||||
|
.moko-social-icons--color-brand .fa-whatsapp { color: #25d366; }
|
||||||
|
.moko-social-icons--color-brand .fa-tumblr { color: #35465c; }
|
||||||
|
.moko-social-icons--color-brand .fa-twitch { color: #9146ff; }
|
||||||
|
.moko-social-icons--color-brand .fa-spotify { color: #1db954; }
|
||||||
|
.moko-social-icons--color-brand .fa-soundcloud { color: #ff5500; }
|
||||||
|
.moko-social-icons--color-brand .fa-flickr { color: #0063dc; }
|
||||||
|
.moko-social-icons--color-brand .fa-vimeo-v { color: #1ab7ea; }
|
||||||
|
|
||||||
|
:root[data-bs-theme="dark"] .moko-social-icons--color-brand .fa-x-twitter,
|
||||||
|
:root[data-bs-theme="dark"] .moko-social-icons--color-brand .fa-threads,
|
||||||
|
:root[data-bs-theme="dark"] .moko-social-icons--color-brand .fa-tiktok,
|
||||||
|
:root[data-bs-theme="dark"] .moko-social-icons--color-brand .fa-github {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style: plain (default) — no background */
|
||||||
|
.moko-social-icons--plain a {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style: square — square background */
|
||||||
|
.moko-social-icons--square a {
|
||||||
|
width: 2.2em;
|
||||||
|
height: 2.2em;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: var(--social-icon-bg, rgba(255, 255, 255, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
.moko-social-icons--square a:hover,
|
||||||
|
.moko-social-icons--square a:focus-visible {
|
||||||
|
background-color: var(--social-icon-hover-bg, rgba(255, 255, 255, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style: circle — round background */
|
||||||
|
.moko-social-icons--circle a {
|
||||||
|
width: 2.2em;
|
||||||
|
height: 2.2em;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--social-icon-bg, rgba(255, 255, 255, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
.moko-social-icons--circle a:hover,
|
||||||
|
.moko-social-icons--circle a:focus-visible {
|
||||||
|
background-color: var(--social-icon-hover-bg, rgba(255, 255, 255, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style: rounded — rounded-rect background */
|
||||||
|
.moko-social-icons--rounded a {
|
||||||
|
width: 2.2em;
|
||||||
|
height: 2.2em;
|
||||||
|
border-radius: var(--social-icon-radius, 0.375rem);
|
||||||
|
background-color: var(--social-icon-bg, rgba(255, 255, 255, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
.moko-social-icons--rounded a:hover,
|
||||||
|
.moko-social-icons--rounded a:focus-visible {
|
||||||
|
background-color: var(--social-icon-hover-bg, rgba(255, 255, 255, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position: topbar — align right */
|
||||||
|
.moko-social-icons--topbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position: footer — centered */
|
||||||
|
.moko-social-icons--footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-social {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.moko-social-icons--topbar {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Position: floating — fixed sidebar, hidden on mobile */
|
||||||
|
.moko-social-icons--floating {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 1030;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moko-social-icons--floating ul {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moko-social-icons--floating-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moko-social-icons--floating-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moko-social-icons--floating-left a {
|
||||||
|
border-radius: 0 var(--social-icon-radius, 0.375rem) var(--social-icon-radius, 0.375rem) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moko-social-icons--floating-right a {
|
||||||
|
border-radius: var(--social-icon-radius, 0.375rem) 0 0 var(--social-icon-radius, 0.375rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.moko-social-icons--floating.moko-social-icons--circle a,
|
||||||
|
.moko-social-icons--floating.moko-social-icons--plain a {
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.moko-social-icons--floating {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* ===== End Social Icons ===== */
|
||||||
|
|||||||
@@ -393,6 +393,11 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Social icons -->
|
||||||
|
<?php if ($params->get('social_footer') || $params->get('social_floating')) :
|
||||||
|
echo \Joomla\CMS\Layout\LayoutHelper::render('mokoonyx.social-icons', ['params' => $params, 'position' => 'footer']);
|
||||||
|
endif; ?>
|
||||||
|
|
||||||
<!-- Copyright -->
|
<!-- Copyright -->
|
||||||
<div class="moko-offline-copyright">
|
<div class="moko-offline-copyright">
|
||||||
<div>© <?php echo date('Y'); ?> <?php echo htmlspecialchars($sitename, ENT_COMPAT, 'UTF-8'); ?></div>
|
<div>© <?php echo date('Y'); ?> <?php echo htmlspecialchars($sitename, ENT_COMPAT, 'UTF-8'); ?></div>
|
||||||
|
|||||||
+282
@@ -36,6 +36,10 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
|
|||||||
|
|
||||||
public function preflight(string $type, InstallerAdapter $parent): bool
|
public function preflight(string $type, InstallerAdapter $parent): bool
|
||||||
{
|
{
|
||||||
|
if ($type === 'update') {
|
||||||
|
$this->saveDownloadKey();
|
||||||
|
}
|
||||||
|
|
||||||
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
|
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
|
||||||
Factory::getApplication()->enqueueMessage(
|
Factory::getApplication()->enqueueMessage(
|
||||||
sprintf('MokoOnyx requires PHP %s or later. You are running PHP %s.', self::MIN_PHP, PHP_VERSION),
|
sprintf('MokoOnyx requires PHP %s or later. You are running PHP %s.', self::MIN_PHP, PHP_VERSION),
|
||||||
@@ -88,11 +92,16 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
|
|||||||
|
|
||||||
public function postflight(string $type, InstallerAdapter $parent): bool
|
public function postflight(string $type, InstallerAdapter $parent): bool
|
||||||
{
|
{
|
||||||
|
$this->restoreDownloadKey();
|
||||||
|
$this->warnMissingLicenseKey();
|
||||||
|
|
||||||
if ($type === 'install' || $type === 'update') {
|
if ($type === 'install' || $type === 'update') {
|
||||||
$this->migrateFromCassiopeia();
|
$this->migrateFromCassiopeia();
|
||||||
$this->replaceCassiopeiaReferences();
|
$this->replaceCassiopeiaReferences();
|
||||||
$this->clearFaviconStamp();
|
$this->clearFaviconStamp();
|
||||||
$this->cleanMediaFolder();
|
$this->cleanMediaFolder();
|
||||||
|
$this->removeDeletedFiles();
|
||||||
|
$this->removeDuplicateExtensions();
|
||||||
$this->lockExtension();
|
$this->lockExtension();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,6 +492,189 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove duplicate MokoOnyx extension entries from #__extensions.
|
||||||
|
*
|
||||||
|
* Re-installs or migrations can leave ghost rows. We keep the one
|
||||||
|
* that is locked (the active template) and delete any extras.
|
||||||
|
* Also removes stale MokoCassiopeia entries if present.
|
||||||
|
*/
|
||||||
|
private function removeDuplicateExtensions(): void
|
||||||
|
{
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
|
||||||
|
// Find all MokoOnyx template entries
|
||||||
|
$rows = $db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->select(['extension_id', 'locked'])
|
||||||
|
->from('#__extensions')
|
||||||
|
->where($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME))
|
||||||
|
->where($db->quoteName('type') . ' = ' . $db->quote('template'))
|
||||||
|
->order('locked DESC, extension_id ASC')
|
||||||
|
)->loadObjectList();
|
||||||
|
|
||||||
|
if (count($rows) > 1) {
|
||||||
|
$keep = (int) $rows[0]->extension_id;
|
||||||
|
$removed = 0;
|
||||||
|
|
||||||
|
for ($i = 1; $i < count($rows); $i++) {
|
||||||
|
$staleId = (int) $rows[$i]->extension_id;
|
||||||
|
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->delete('#__update_sites_extensions')
|
||||||
|
->where('extension_id = ' . $staleId)
|
||||||
|
)->execute();
|
||||||
|
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->delete('#__extensions')
|
||||||
|
->where('extension_id = ' . $staleId)
|
||||||
|
)->execute();
|
||||||
|
|
||||||
|
$removed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($removed > 0) {
|
||||||
|
$this->logMessage("Removed {$removed} duplicate MokoOnyx extension(s). Kept ID {$keep}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove faulty "template-mokoonyx" duplicate (wrong element name from bad install)
|
||||||
|
$faultyId = (int) $db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->select('extension_id')
|
||||||
|
->from('#__extensions')
|
||||||
|
->where($db->quoteName('element') . ' = ' . $db->quote('template-mokoonyx'))
|
||||||
|
->where($db->quoteName('type') . ' = ' . $db->quote('template'))
|
||||||
|
)->loadResult();
|
||||||
|
|
||||||
|
if ($faultyId) {
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->delete('#__update_sites_extensions')
|
||||||
|
->where('extension_id = ' . $faultyId)
|
||||||
|
)->execute();
|
||||||
|
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->delete('#__extensions')
|
||||||
|
->where('extension_id = ' . $faultyId)
|
||||||
|
)->execute();
|
||||||
|
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->delete('#__template_styles')
|
||||||
|
->where($db->quoteName('template') . ' = ' . $db->quote('template-mokoonyx'))
|
||||||
|
)->execute();
|
||||||
|
|
||||||
|
$this->logMessage('Removed faulty template-mokoonyx duplicate extension.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove stale MokoCassiopeia if not set as default
|
||||||
|
$oldExt = (int) $db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->select('extension_id')
|
||||||
|
->from('#__extensions')
|
||||||
|
->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
|
||||||
|
->where($db->quoteName('type') . ' = ' . $db->quote('template'))
|
||||||
|
)->loadResult();
|
||||||
|
|
||||||
|
if ($oldExt) {
|
||||||
|
$isDefault = (int) $db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->select('COUNT(*)')
|
||||||
|
->from('#__template_styles')
|
||||||
|
->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME))
|
||||||
|
->where($db->quoteName('home') . ' = 1')
|
||||||
|
)->loadResult();
|
||||||
|
|
||||||
|
if ($isDefault === 0) {
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->delete('#__update_sites_extensions')
|
||||||
|
->where('extension_id = ' . $oldExt)
|
||||||
|
)->execute();
|
||||||
|
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->delete('#__extensions')
|
||||||
|
->where('extension_id = ' . $oldExt)
|
||||||
|
)->execute();
|
||||||
|
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->delete('#__template_styles')
|
||||||
|
->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME))
|
||||||
|
)->execute();
|
||||||
|
|
||||||
|
$this->logMessage('Removed stale MokoCassiopeia extension and styles.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove files and directories that were shipped in previous versions
|
||||||
|
* but have since been deleted from the package.
|
||||||
|
*
|
||||||
|
* Joomla's installer never deletes files on upgrade — it only
|
||||||
|
* adds/overwrites. This method fills that gap so stale overrides
|
||||||
|
* and deprecated assets don't linger on disk.
|
||||||
|
*
|
||||||
|
* Maintain this list: when you delete a file from the repo, add its
|
||||||
|
* path here (relative to the template root) so existing installs
|
||||||
|
* get cleaned up on the next update.
|
||||||
|
*/
|
||||||
|
private function removeDeletedFiles(): void
|
||||||
|
{
|
||||||
|
$templateRoot = JPATH_ROOT . '/templates/' . self::NEW_NAME;
|
||||||
|
|
||||||
|
// Paths relative to templates/mokoonyx/
|
||||||
|
$deletedFiles = [
|
||||||
|
// JoomGallery template overrides — removed in 02.19.00
|
||||||
|
'html/com_joomgallery/category/default.php',
|
||||||
|
'html/com_joomgallery/category/default_cat.php',
|
||||||
|
'html/com_joomgallery/category/index.html',
|
||||||
|
'html/com_joomgallery/gallery/default.php',
|
||||||
|
'html/com_joomgallery/gallery/index.html',
|
||||||
|
'html/com_joomgallery/image/default.php',
|
||||||
|
'html/com_joomgallery/image/index.html',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Directories to remove (only if empty after file deletion)
|
||||||
|
$deletedDirs = [
|
||||||
|
'html/com_joomgallery/image',
|
||||||
|
'html/com_joomgallery/gallery',
|
||||||
|
'html/com_joomgallery/category',
|
||||||
|
'html/com_joomgallery',
|
||||||
|
];
|
||||||
|
|
||||||
|
$removed = 0;
|
||||||
|
|
||||||
|
foreach ($deletedFiles as $relPath) {
|
||||||
|
$file = $templateRoot . '/' . $relPath;
|
||||||
|
if (is_file($file)) {
|
||||||
|
@unlink($file);
|
||||||
|
$removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($deletedDirs as $relPath) {
|
||||||
|
$dir = $templateRoot . '/' . $relPath;
|
||||||
|
if (is_dir($dir)) {
|
||||||
|
// Only remove if empty
|
||||||
|
$entries = @scandir($dir);
|
||||||
|
if ($entries && count($entries) <= 2) { // . and .. only
|
||||||
|
@rmdir($dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($removed > 0) {
|
||||||
|
$this->logMessage("Removed {$removed} deprecated file(s) from previous versions.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function logMessage(string $message, string $priority = 'info'): void
|
private function logMessage(string $message, string $priority = 'info'): void
|
||||||
{
|
{
|
||||||
$priorities = [
|
$priorities = [
|
||||||
@@ -499,4 +691,94 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
|
|||||||
|
|
||||||
Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokoonyx');
|
Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokoonyx');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ?string $savedDownloadKey = null;
|
||||||
|
|
||||||
|
private function saveDownloadKey(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->select($db->quoteName('us.extra_query'))
|
||||||
|
->from($db->quoteName('#__update_sites', 'us'))
|
||||||
|
->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id')
|
||||||
|
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id')
|
||||||
|
->where($db->quoteName('e.element') . ' = ' . $db->quote('mokoonyx'))
|
||||||
|
->setLimit(1)
|
||||||
|
);
|
||||||
|
$key = $db->loadResult();
|
||||||
|
if (!empty($key)) {
|
||||||
|
$this->savedDownloadKey = $key;
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function restoreDownloadKey(): void
|
||||||
|
{
|
||||||
|
if ($this->savedDownloadKey === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->select($db->quoteName('us.update_site_id'))
|
||||||
|
->from($db->quoteName('#__update_sites', 'us'))
|
||||||
|
->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id')
|
||||||
|
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id')
|
||||||
|
->where($db->quoteName('e.element') . ' = ' . $db->quote('mokoonyx'))
|
||||||
|
->setLimit(1)
|
||||||
|
);
|
||||||
|
$siteId = (int) $db->loadResult();
|
||||||
|
if ($siteId > 0) {
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->update($db->quoteName('#__update_sites'))
|
||||||
|
->set($db->quoteName('extra_query') . ' = ' . $db->quote($this->savedDownloadKey))
|
||||||
|
->where($db->quoteName('update_site_id') . ' = ' . $siteId)
|
||||||
|
)->execute();
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function warnMissingLicenseKey(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
$db->setQuery(
|
||||||
|
$db->getQuery(true)
|
||||||
|
->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')])
|
||||||
|
->from($db->quoteName('#__update_sites'))
|
||||||
|
->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoOnyx%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoOnyx%') . ')')
|
||||||
|
->setLimit(1)
|
||||||
|
);
|
||||||
|
$site = $db->loadObject();
|
||||||
|
|
||||||
|
if ($site) {
|
||||||
|
$eq = (string) ($site->extra_query ?? '');
|
||||||
|
if (!empty($eq) && strpos($eq, 'dlid=') !== false) {
|
||||||
|
parse_str($eq, $p);
|
||||||
|
if (!empty($p['dlid'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . (int) $site->update_site_id;
|
||||||
|
} else {
|
||||||
|
$editUrl = 'index.php?option=com_installer&view=updatesites';
|
||||||
|
}
|
||||||
|
|
||||||
|
Factory::getApplication()->enqueueMessage(
|
||||||
|
'<strong>Moko Consulting License Key Required</strong> — '
|
||||||
|
. 'No download key is configured. Updates will not be available until a valid license key is entered. '
|
||||||
|
. '<a href="' . htmlspecialchars($editUrl, ENT_QUOTES, 'UTF-8') . '" class="btn btn-sm btn-warning ms-2">Enter License Key</a>',
|
||||||
|
'warning'
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+86
-8
@@ -31,16 +31,11 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="template" client="site" method="upgrade">
|
<extension type="template" client="site" method="upgrade">
|
||||||
<updateservers>
|
<updateservers>
|
||||||
<server type="extension" priority="1" name="MokoOnyx Update Server (MokoGitea)">
|
<server type="extension" name="Template - MokoOnyx">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/updates.xml</server>
|
||||||
https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml
|
|
||||||
</server>
|
|
||||||
</updateservers>
|
</updateservers>
|
||||||
|
<dlid prefix="dlid=" suffix=""/>
|
||||||
<name>mokoonyx</name>
|
<name>mokoonyx</name>
|
||||||
<<<<<<< HEAD
|
<version>02.21.05-dev</version>
|
||||||
<version>02.15.02-dev</version>
|
|
||||||
=======
|
|
||||||
<version>02.15.02-dev</version>
|
|
||||||
>>>>>>> origin/main
|
|
||||||
<scriptfile>script.php</scriptfile>
|
<scriptfile>script.php</scriptfile>
|
||||||
<creationDate>2026-05-16</creationDate>
|
<creationDate>2026-05-16</creationDate>
|
||||||
<author>Jonathan Miller || Moko Consulting</author>
|
<author>Jonathan Miller || Moko Consulting</author>
|
||||||
@@ -329,6 +324,88 @@
|
|||||||
</field>
|
</field>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Social icons tab -->
|
||||||
|
<fieldset name="social" label="TPL_MOKOONYX_SOCIAL_FIELDSET_LABEL">
|
||||||
|
<field name="social_note" type="note" description="TPL_MOKOONYX_SOCIAL_NOTE" />
|
||||||
|
|
||||||
|
<!-- Display toggles -->
|
||||||
|
<field name="social_sep_display" type="spacer" label="Display" hr="false" class="text fw-bold" />
|
||||||
|
<field name="social_topbar" type="radio" default="0"
|
||||||
|
label="TPL_MOKOONYX_SOCIAL_TOPBAR_LABEL" description="TPL_MOKOONYX_SOCIAL_TOPBAR_DESC"
|
||||||
|
layout="joomla.form.field.radio.switcher" filter="boolean">
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field name="social_footer" type="radio" default="0"
|
||||||
|
label="TPL_MOKOONYX_SOCIAL_FOOTER_LABEL" description="TPL_MOKOONYX_SOCIAL_FOOTER_DESC"
|
||||||
|
layout="joomla.form.field.radio.switcher" filter="boolean">
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field name="social_floating" type="radio" default="0"
|
||||||
|
label="TPL_MOKOONYX_SOCIAL_FLOATING_LABEL" description="TPL_MOKOONYX_SOCIAL_FLOATING_DESC"
|
||||||
|
layout="joomla.form.field.radio.switcher" filter="boolean">
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
</field>
|
||||||
|
<field name="social_floating_pos" type="list" default="left"
|
||||||
|
label="TPL_MOKOONYX_SOCIAL_FLOATING_POS_LABEL" description="TPL_MOKOONYX_SOCIAL_FLOATING_POS_DESC"
|
||||||
|
showon="social_floating:1">
|
||||||
|
<option value="left">TPL_MOKOONYX_SOCIAL_FLOATING_POS_LEFT</option>
|
||||||
|
<option value="right">TPL_MOKOONYX_SOCIAL_FLOATING_POS_RIGHT</option>
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<!-- Style options -->
|
||||||
|
<field name="social_sep_style" type="spacer" label="Style" hr="false" class="text fw-bold" />
|
||||||
|
<field name="social_icon_style" type="list" default="plain"
|
||||||
|
label="TPL_MOKOONYX_SOCIAL_STYLE_LABEL" description="TPL_MOKOONYX_SOCIAL_STYLE_DESC">
|
||||||
|
<option value="plain">TPL_MOKOONYX_SOCIAL_STYLE_PLAIN</option>
|
||||||
|
<option value="square">TPL_MOKOONYX_SOCIAL_STYLE_SQUARE</option>
|
||||||
|
<option value="circle">TPL_MOKOONYX_SOCIAL_STYLE_CIRCLE</option>
|
||||||
|
<option value="rounded">TPL_MOKOONYX_SOCIAL_STYLE_ROUNDED</option>
|
||||||
|
</field>
|
||||||
|
<field name="social_icon_size" type="list" default="md"
|
||||||
|
label="TPL_MOKOONYX_SOCIAL_SIZE_LABEL" description="TPL_MOKOONYX_SOCIAL_SIZE_DESC">
|
||||||
|
<option value="sm">TPL_MOKOONYX_SOCIAL_SIZE_SM</option>
|
||||||
|
<option value="md">TPL_MOKOONYX_SOCIAL_SIZE_MD</option>
|
||||||
|
<option value="lg">TPL_MOKOONYX_SOCIAL_SIZE_LG</option>
|
||||||
|
</field>
|
||||||
|
<field name="social_icon_color" type="list" default="theme"
|
||||||
|
label="TPL_MOKOONYX_SOCIAL_COLOR_LABEL" description="TPL_MOKOONYX_SOCIAL_COLOR_DESC">
|
||||||
|
<option value="theme">TPL_MOKOONYX_SOCIAL_COLOR_THEME</option>
|
||||||
|
<option value="brand">TPL_MOKOONYX_SOCIAL_COLOR_BRAND</option>
|
||||||
|
<option value="black">TPL_MOKOONYX_SOCIAL_COLOR_BLACK</option>
|
||||||
|
<option value="white">TPL_MOKOONYX_SOCIAL_COLOR_WHITE</option>
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<!-- Platform URLs -->
|
||||||
|
<field name="social_sep_urls" type="spacer" label="Platform URLs" hr="false" class="text fw-bold" />
|
||||||
|
<field name="social_urls_note" type="note" description="TPL_MOKOONYX_SOCIAL_URLS_NOTE" />
|
||||||
|
<field name="social_facebook_url" type="url" default="" label="Facebook" filter="url" />
|
||||||
|
<field name="social_twitter_url" type="url" default="" label="X / Twitter" filter="url" />
|
||||||
|
<field name="social_instagram_url" type="url" default="" label="Instagram" filter="url" />
|
||||||
|
<field name="social_linkedin_url" type="url" default="" label="LinkedIn" filter="url" />
|
||||||
|
<field name="social_youtube_url" type="url" default="" label="YouTube" filter="url" />
|
||||||
|
<field name="social_github_url" type="url" default="" label="GitHub" filter="url" />
|
||||||
|
<field name="social_bluesky_url" type="url" default="" label="Bluesky" filter="url" />
|
||||||
|
<field name="social_threads_url" type="url" default="" label="Threads" filter="url" />
|
||||||
|
<field name="social_discord_url" type="url" default="" label="Discord" filter="url" />
|
||||||
|
<field name="social_tiktok_url" type="url" default="" label="TikTok" filter="url" />
|
||||||
|
<field name="social_reddit_url" type="url" default="" label="Reddit" filter="url" />
|
||||||
|
<field name="social_pinterest_url" type="url" default="" label="Pinterest" filter="url" />
|
||||||
|
<field name="social_snapchat_url" type="url" default="" label="Snapchat" filter="url" />
|
||||||
|
<field name="social_telegram_url" type="url" default="" label="Telegram" filter="url" />
|
||||||
|
<field name="social_whatsapp_url" type="url" default="" label="WhatsApp" filter="url" />
|
||||||
|
<field name="social_tumblr_url" type="url" default="" label="Tumblr" filter="url" />
|
||||||
|
<field name="social_twitch_url" type="url" default="" label="Twitch" filter="url" />
|
||||||
|
<field name="social_spotify_url" type="url" default="" label="Spotify" filter="url" />
|
||||||
|
<field name="social_soundcloud_url" type="url" default="" label="SoundCloud" filter="url" />
|
||||||
|
<field name="social_flickr_url" type="url" default="" label="Flickr" filter="url" />
|
||||||
|
<field name="social_vimeo_url" type="url" default="" label="Vimeo" filter="url" />
|
||||||
|
<field name="social_linktree_url" type="url" default="" label="Linktree" filter="url" />
|
||||||
|
<field name="social_mail_url" type="url" default="" label="Email" description="TPL_MOKOONYX_SOCIAL_MAIL_DESC" filter="string" />
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<!-- CSS Variables reference tab -->
|
<!-- CSS Variables reference tab -->
|
||||||
<fieldset name="css_variables" label="TPL_MOKOONYX_CSS_VARS_FIELDSET_LABEL">
|
<fieldset name="css_variables" label="TPL_MOKOONYX_CSS_VARS_FIELDSET_LABEL">
|
||||||
<field name="css_vars_intro" type="note" description="TPL_MOKOONYX_CSS_VARS_INTRO" />
|
<field name="css_vars_intro" type="note" description="TPL_MOKOONYX_CSS_VARS_INTRO" />
|
||||||
@@ -374,6 +451,7 @@
|
|||||||
<field name="css_vars_virtuemart" type="note" label="TPL_MOKOONYX_CSS_VARS_VM_LABEL" description="TPL_MOKOONYX_CSS_VARS_VM_DESC" class="alert alert-light" />
|
<field name="css_vars_virtuemart" type="note" label="TPL_MOKOONYX_CSS_VARS_VM_LABEL" description="TPL_MOKOONYX_CSS_VARS_VM_DESC" class="alert alert-light" />
|
||||||
<field name="css_vars_gable" type="note" label="TPL_MOKOONYX_CSS_VARS_GABLE_LABEL" description="TPL_MOKOONYX_CSS_VARS_GABLE_DESC" class="alert alert-light" />
|
<field name="css_vars_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" />
|
||||||
|
<field name="css_vars_social" type="note" label="TPL_MOKOONYX_CSS_VARS_SOCIAL_LABEL" description="TPL_MOKOONYX_CSS_VARS_SOCIAL_DESC" class="alert alert-info" />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</fields>
|
</fields>
|
||||||
</config>
|
</config>
|
||||||
|
|||||||
-108
@@ -1,108 +0,0 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
|
||||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
VERSION: 02.16.00
|
|
||||||
-->
|
|
||||||
|
|
||||||
<updates>
|
|
||||||
<update>
|
|
||||||
<name>Template - MokoOnyx</name>
|
|
||||||
<description>Template - MokoOnyx dev build.</description>
|
|
||||||
<element>mokoonyx</element>
|
|
||||||
<type>template</type>
|
|
||||||
<client>site</client>
|
|
||||||
<version>02.16.00-dev</version>
|
|
||||||
<creationDate>2026-05-30</creationDate>
|
|
||||||
<infourl title="Template - MokoOnyx">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/development</infourl>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/tpl_mokoonyx-02.16.00-dev.zip</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<sha256>f8eb73b0a0b61a9f37ee5b720182374cb0b2495fda5e1198711d1298ef5bbd67</sha256>
|
|
||||||
<tags><tag>dev</tag></tags>
|
|
||||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md</changelogurl>
|
|
||||||
<maintainer>Moko Consulting</maintainer>
|
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
|
||||||
<targetplatform name="joomla" version="(5|6)\..*"/>
|
|
||||||
<php_minimum>8.1.0</php_minimum>
|
|
||||||
</update>
|
|
||||||
<update>
|
|
||||||
<name>Template - MokoOnyx</name>
|
|
||||||
<description>Template - MokoOnyx alpha build.</description>
|
|
||||||
<element>mokoonyx</element>
|
|
||||||
<type>template</type>
|
|
||||||
<client>site</client>
|
|
||||||
<version>02.16.00-alpha</version>
|
|
||||||
<creationDate>2026-05-30</creationDate>
|
|
||||||
<infourl title="Template - MokoOnyx">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/alpha</infourl>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/alpha/tpl_mokoonyx-02.16.00-alpha.zip</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<sha256>f8eb73b0a0b61a9f37ee5b720182374cb0b2495fda5e1198711d1298ef5bbd67</sha256>
|
|
||||||
<tags><tag>alpha</tag></tags>
|
|
||||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md</changelogurl>
|
|
||||||
<maintainer>Moko Consulting</maintainer>
|
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
|
||||||
<targetplatform name="joomla" version="(5|6)\..*"/>
|
|
||||||
<php_minimum>8.1.0</php_minimum>
|
|
||||||
</update>
|
|
||||||
<update>
|
|
||||||
<name>Template - MokoOnyx</name>
|
|
||||||
<description>Template - MokoOnyx beta build.</description>
|
|
||||||
<element>mokoonyx</element>
|
|
||||||
<type>template</type>
|
|
||||||
<client>site</client>
|
|
||||||
<version>02.16.00-beta</version>
|
|
||||||
<creationDate>2026-05-30</creationDate>
|
|
||||||
<infourl title="Template - MokoOnyx">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/beta</infourl>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/beta/tpl_mokoonyx-02.16.00-beta.zip</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<sha256>f8eb73b0a0b61a9f37ee5b720182374cb0b2495fda5e1198711d1298ef5bbd67</sha256>
|
|
||||||
<tags><tag>beta</tag></tags>
|
|
||||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md</changelogurl>
|
|
||||||
<maintainer>Moko Consulting</maintainer>
|
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
|
||||||
<targetplatform name="joomla" version="(5|6)\..*"/>
|
|
||||||
<php_minimum>8.1.0</php_minimum>
|
|
||||||
</update>
|
|
||||||
<update>
|
|
||||||
<name>Template - MokoOnyx</name>
|
|
||||||
<description>Template - MokoOnyx rc build.</description>
|
|
||||||
<element>mokoonyx</element>
|
|
||||||
<type>template</type>
|
|
||||||
<client>site</client>
|
|
||||||
<version>02.16.00-rc</version>
|
|
||||||
<creationDate>2026-05-30</creationDate>
|
|
||||||
<infourl title="Template - MokoOnyx">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/release-candidate</infourl>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/release-candidate/tpl_mokoonyx-02.16.00-rc.zip</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<sha256>f8eb73b0a0b61a9f37ee5b720182374cb0b2495fda5e1198711d1298ef5bbd67</sha256>
|
|
||||||
<tags><tag>rc</tag></tags>
|
|
||||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md</changelogurl>
|
|
||||||
<maintainer>Moko Consulting</maintainer>
|
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
|
||||||
<targetplatform name="joomla" version="(5|6)\..*"/>
|
|
||||||
<php_minimum>8.1.0</php_minimum>
|
|
||||||
</update>
|
|
||||||
<update>
|
|
||||||
<name>Template - MokoOnyx</name>
|
|
||||||
<description>Template - MokoOnyx stable build.</description>
|
|
||||||
<element>mokoonyx</element>
|
|
||||||
<type>template</type>
|
|
||||||
<client>site</client>
|
|
||||||
<version>02.16.00</version>
|
|
||||||
<creationDate>2026-05-30</creationDate>
|
|
||||||
<infourl title='Template - MokoOnyx'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/stable</infourl>
|
|
||||||
<downloads>
|
|
||||||
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/stable/tpl_mokoonyx-02.16.00.zip</downloadurl>
|
|
||||||
</downloads>
|
|
||||||
<sha256>f8eb73b0a0b61a9f37ee5b720182374cb0b2495fda5e1198711d1298ef5bbd67</sha256>
|
|
||||||
<tags><tag>stable</tag></tags>
|
|
||||||
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md</changelogurl>
|
|
||||||
<maintainer>Moko Consulting</maintainer>
|
|
||||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
|
||||||
<targetplatform name="joomla" version="(5|6)\..*" />
|
|
||||||
<php_minimum>8.1.0</php_minimum>
|
|
||||||
</update>
|
|
||||||
</updates>
|
|
||||||
@@ -136,11 +136,11 @@ A read-only reference tab displaying all available CSS custom properties organiz
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
@@ -223,11 +223,11 @@ Additional variables are defined for: VirtueMart (`--vm-*`), Gable (`--gab-*`),
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
@@ -114,11 +114,11 @@ For additional overrides beyond theme variables, use these files (also update-sa
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
+5
-5
@@ -46,7 +46,7 @@ MokoOnyx/
|
|||||||
│ ├── dark.custom.css # Custom dark palette template
|
│ ├── dark.custom.css # Custom dark palette template
|
||||||
│ └── brand-showcase.html # Brand showcase HTML template
|
│ └── brand-showcase.html # Brand showcase HTML template
|
||||||
├── Makefile # Build and validation automation
|
├── Makefile # Build and validation automation
|
||||||
├── composer.json # PHP dependencies (MokoStandards)
|
├── composer.json # PHP dependencies (moko-platform)
|
||||||
├── package.json # Node.js dependencies (minification)
|
├── package.json # Node.js dependencies (minification)
|
||||||
├── phpcs.xml # PHP CodeSniffer configuration
|
├── phpcs.xml # PHP CodeSniffer configuration
|
||||||
├── phpstan.neon # PHPStan static analysis configuration
|
├── phpstan.neon # PHPStan static analysis configuration
|
||||||
@@ -63,7 +63,7 @@ MokoOnyx/
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **PHP** 8.1+
|
- **PHP** 8.1+
|
||||||
- **Composer** (for MokoStandards CLI and dependencies)
|
- **Composer** (for moko-platform CLI and dependencies)
|
||||||
- **Node.js** (optional, for build-time minification with terser/clean-css)
|
- **Node.js** (optional, for build-time minification with terser/clean-css)
|
||||||
- **Make** (GNU Make or compatible)
|
- **Make** (GNU Make or compatible)
|
||||||
- **zip** (or PowerShell for Windows)
|
- **zip** (or PowerShell for Windows)
|
||||||
@@ -110,7 +110,7 @@ Creates `dist/mokoonyx-{version}-beta.zip` (skips minification).
|
|||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
MokoOnyx uses the **MokoStandards Enterprise API** for code quality checks.
|
MokoOnyx uses the **moko-platform CLI** for code quality checks.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all validation checks
|
# Run all validation checks
|
||||||
@@ -277,11 +277,11 @@ vendor/bin/codecept run
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
+2
-2
@@ -88,11 +88,11 @@ Key parameters include:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> **[MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki)** -- central standards hub for all Moko Consulting projects.
|
> **[moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki)** -- central standards hub for all Moko Consulting projects.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ MokoOnyx includes an automatic update server. Joomla will notify you when new ve
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
+2
-2
@@ -108,11 +108,11 @@ Once you have confirmed everything is working:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
@@ -102,11 +102,11 @@ This immediately deletes all `.min` files. The template will load the unminified
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
@@ -99,11 +99,11 @@ Each layout has sub-templates for different menu item types: `_component`, `_hea
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -111,7 +111,7 @@ Each layout has sub-templates for different menu item types: `_component`, `_hea
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
|||||||
Reference in New Issue
Block a user