18 Commits

Author SHA1 Message Date
8c25461f15 chore: update update.xml from MokoStandards
Some checks failed
CodeQL Security Scanning / Analyze (actions) (push) Has been cancelled
CodeQL Security Scanning / Analyze (javascript) (push) Has been cancelled
CodeQL Security Scanning / Security Scan Summary (push) Has been cancelled
Standards Compliance / Secret Scanning (push) Has been cancelled
Standards Compliance / License Header Validation (push) Has been cancelled
Standards Compliance / Repository Structure Validation (push) Has been cancelled
Standards Compliance / Coding Standards Check (push) Has been cancelled
Standards Compliance / Version Consistency Check (push) Has been cancelled
Standards Compliance / Workflow Configuration Check (push) Has been cancelled
Standards Compliance / Documentation Quality Check (push) Has been cancelled
Standards Compliance / README Completeness Check (push) Has been cancelled
Standards Compliance / Git Repository Hygiene (push) Has been cancelled
Standards Compliance / Script Integrity Validation (push) Has been cancelled
Standards Compliance / Line Length Check (push) Has been cancelled
Standards Compliance / File Naming Standards (push) Has been cancelled
Standards Compliance / Insecure Code Pattern Detection (push) Has been cancelled
Standards Compliance / Code Complexity Analysis (push) Has been cancelled
Standards Compliance / Code Duplication Detection (push) Has been cancelled
Standards Compliance / Dead Code Detection (push) Has been cancelled
Standards Compliance / File Size Limits (push) Has been cancelled
Standards Compliance / Binary File Detection (push) Has been cancelled
Standards Compliance / TODO/FIXME Tracking (push) Has been cancelled
Standards Compliance / Dependency Vulnerability Scanning (push) Has been cancelled
Standards Compliance / Unused Dependencies Check (push) Has been cancelled
Standards Compliance / Broken Link Detection (push) Has been cancelled
Standards Compliance / API Documentation Coverage (push) Has been cancelled
Standards Compliance / Accessibility Check (push) Has been cancelled
Standards Compliance / Performance Metrics (push) Has been cancelled
Standards Compliance / Enterprise Readiness Check (push) Has been cancelled
Standards Compliance / Repository Health Check (push) Has been cancelled
Standards Compliance / Terraform Configuration Validation (push) Has been cancelled
Standards Compliance / Compliance Summary (push) Has been cancelled
Repo Health / Access control (push) Failing after 2s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
2026-04-17 10:39:16 +00:00
8a02eb127d chore: update update.xml from MokoStandards
Some checks failed
CodeQL Security Scanning / Analyze (actions) (push) Has been cancelled
CodeQL Security Scanning / Analyze (javascript) (push) Has been cancelled
CodeQL Security Scanning / Security Scan Summary (push) Has been cancelled
Repo Health / Access control (push) Has been cancelled
Repo Health / Release configuration (push) Has been cancelled
Repo Health / Scripts governance (push) Has been cancelled
Repo Health / Repository health (push) Has been cancelled
Standards Compliance / Secret Scanning (push) Has started running
Standards Compliance / License Header Validation (push) Has started running
Standards Compliance / Repository Structure Validation (push) Has been cancelled
Standards Compliance / Coding Standards Check (push) Has been cancelled
Standards Compliance / Version Consistency Check (push) Has been cancelled
Standards Compliance / Workflow Configuration Check (push) Has been cancelled
Standards Compliance / Documentation Quality Check (push) Has been cancelled
Standards Compliance / README Completeness Check (push) Has been cancelled
Standards Compliance / Git Repository Hygiene (push) Has been cancelled
Standards Compliance / Script Integrity Validation (push) Has been cancelled
Standards Compliance / Line Length Check (push) Has been cancelled
Standards Compliance / File Naming Standards (push) Has been cancelled
Standards Compliance / Insecure Code Pattern Detection (push) Has been cancelled
Standards Compliance / Code Complexity Analysis (push) Has been cancelled
Standards Compliance / Code Duplication Detection (push) Has been cancelled
Standards Compliance / Dead Code Detection (push) Has been cancelled
Standards Compliance / File Size Limits (push) Has been cancelled
Standards Compliance / Binary File Detection (push) Has been cancelled
Standards Compliance / TODO/FIXME Tracking (push) Has been cancelled
Standards Compliance / Dependency Vulnerability Scanning (push) Has been cancelled
Standards Compliance / Unused Dependencies Check (push) Has been cancelled
Standards Compliance / Broken Link Detection (push) Has been cancelled
Standards Compliance / API Documentation Coverage (push) Has been cancelled
Standards Compliance / Accessibility Check (push) Has been cancelled
Standards Compliance / Performance Metrics (push) Has been cancelled
Standards Compliance / Enterprise Readiness Check (push) Has been cancelled
Standards Compliance / Repository Health Check (push) Has been cancelled
Standards Compliance / Terraform Configuration Validation (push) Has been cancelled
Standards Compliance / Compliance Summary (push) Has been cancelled
2026-04-17 10:33:08 +00:00
095b9412ff chore: update update.xml from MokoStandards
Some checks failed
Repo Health / Access control (push) Failing after 5s
Standards Compliance / Secret Scanning (push) Successful in 3s
Standards Compliance / License Header Validation (push) Successful in 2s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 4s
Standards Compliance / Version Consistency Check (push) Successful in 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 4s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 3s
Standards Compliance / Git Repository Hygiene (push) Successful in 4s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 4s
Standards Compliance / File Naming Standards (push) Successful in 2s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Code Complexity Analysis (push) Successful in 4s
CodeQL Security Scanning / Security Scan Summary (push) Has been cancelled
CodeQL Security Scanning / Analyze (actions) (push) Has been cancelled
CodeQL Security Scanning / Analyze (javascript) (push) Has been cancelled
Standards Compliance / Code Duplication Detection (push) Successful in 4s
Repo Health / Release configuration (push) Has been cancelled
Repo Health / Scripts governance (push) Has been cancelled
Repo Health / Repository health (push) Has been cancelled
Standards Compliance / File Size Limits (push) Has been cancelled
Standards Compliance / Binary File Detection (push) Has been cancelled
Standards Compliance / TODO/FIXME Tracking (push) Has been cancelled
Standards Compliance / Dependency Vulnerability Scanning (push) Has been cancelled
Standards Compliance / Unused Dependencies Check (push) Has been cancelled
Standards Compliance / Broken Link Detection (push) Has been cancelled
Standards Compliance / API Documentation Coverage (push) Has been cancelled
Standards Compliance / Accessibility Check (push) Has been cancelled
Standards Compliance / Performance Metrics (push) Has been cancelled
Standards Compliance / Enterprise Readiness Check (push) Has been cancelled
Standards Compliance / Dead Code Detection (push) Has been cancelled
Standards Compliance / Repository Health Check (push) Has been cancelled
Standards Compliance / Terraform Configuration Validation (push) Has been cancelled
Standards Compliance / Compliance Summary (push) Has been cancelled
2026-04-17 10:31:35 +00:00
b9de0ef88a chore: update update.xml from MokoStandards
Some checks failed
Repo Health / Access control (push) Failing after 1s
Standards Compliance / Secret Scanning (push) Successful in 3s
Standards Compliance / License Header Validation (push) Successful in 3s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Version Consistency Check (push) Successful in 3s
Standards Compliance / Workflow Configuration Check (push) Failing after 3s
Standards Compliance / Documentation Quality Check (push) Successful in 3s
Standards Compliance / README Completeness Check (push) Successful in 2s
Standards Compliance / Git Repository Hygiene (push) Successful in 3s
Standards Compliance / Script Integrity Validation (push) Successful in 4s
Standards Compliance / Line Length Check (push) Failing after 3s
Standards Compliance / File Naming Standards (push) Successful in 3s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 3s
Standards Compliance / Code Complexity Analysis (push) Successful in 3s
Standards Compliance / Code Duplication Detection (push) Successful in 4s
Standards Compliance / Dead Code Detection (push) Successful in 4s
Standards Compliance / File Size Limits (push) Successful in 3s
Standards Compliance / Binary File Detection (push) Successful in 3s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 5s
Standards Compliance / Unused Dependencies Check (push) Successful in 6s
Standards Compliance / Broken Link Detection (push) Successful in 5s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 3s
Standards Compliance / Enterprise Readiness Check (push) Failing after 3s
Standards Compliance / Repository Health Check (push) Failing after 3s
Standards Compliance / Terraform Configuration Validation (push) Successful in 10s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
Standards Compliance / Compliance Summary (push) Failing after 1s
CodeQL Security Scanning / Security Scan Summary (push) Has been cancelled
CodeQL Security Scanning / Analyze (javascript) (push) Has been cancelled
CodeQL Security Scanning / Analyze (actions) (push) Has been cancelled
2026-04-17 10:26:30 +00:00
2c2ae1c02f chore: add update.xml from MokoStandards
Some checks failed
Repo Health / Access control (push) Failing after 4s
Standards Compliance / Secret Scanning (push) Successful in 7s
Standards Compliance / License Header Validation (push) Successful in 2s
Standards Compliance / Repository Structure Validation (push) Successful in 3s
Standards Compliance / Coding Standards Check (push) Failing after 3s
Standards Compliance / Version Consistency Check (push) Successful in 3s
CodeQL Security Scanning / Security Scan Summary (push) Has been cancelled
CodeQL Security Scanning / Analyze (actions) (push) Has been cancelled
CodeQL Security Scanning / Analyze (javascript) (push) Has been cancelled
Repo Health / Release configuration (push) Has been cancelled
Repo Health / Scripts governance (push) Has been cancelled
Repo Health / Repository health (push) Has been cancelled
Standards Compliance / Workflow Configuration Check (push) Has been cancelled
Standards Compliance / Git Repository Hygiene (push) Successful in 4s
Standards Compliance / README Completeness Check (push) Successful in 6s
Standards Compliance / Documentation Quality Check (push) Successful in 8s
Standards Compliance / File Naming Standards (push) Successful in 3s
Standards Compliance / Line Length Check (push) Failing after 4s
Standards Compliance / Insecure Code Pattern Detection (push) Successful in 2s
Standards Compliance / Code Complexity Analysis (push) Successful in 4s
Standards Compliance / Code Duplication Detection (push) Successful in 3s
Standards Compliance / File Size Limits (push) Successful in 3s
Standards Compliance / Binary File Detection (push) Successful in 3s
Standards Compliance / TODO/FIXME Tracking (push) Successful in 3s
Standards Compliance / Script Integrity Validation (push) Successful in 43s
Standards Compliance / Unused Dependencies Check (push) Successful in 4s
Standards Compliance / Broken Link Detection (push) Successful in 4s
Standards Compliance / Dead Code Detection (push) Successful in 43s
Standards Compliance / API Documentation Coverage (push) Successful in 3s
Standards Compliance / Accessibility Check (push) Successful in 3s
Standards Compliance / Performance Metrics (push) Successful in 2s
Standards Compliance / Enterprise Readiness Check (push) Failing after 4s
Standards Compliance / Repository Health Check (push) Failing after 3s
Standards Compliance / Terraform Configuration Validation (push) Successful in 12s
Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 50s
Standards Compliance / Compliance Summary (push) Failing after 1s
2026-04-17 10:26:01 +00:00
Jonathan Miller
5b4151310a Fix updates.xml + release workflow heredoc [skip ci] 2026-04-17 05:15:38 -05:00
Jonathan Miller
0a920d1606 Fix: COMPOSER_AUTH for Gitea registry [skip ci] 2026-04-16 23:52:01 -05:00
Jonathan Miller
b02181ebd0 Replace shivammathur/setup-php — PHP in runner image [skip ci] 2026-04-16 23:20:54 -05:00
Jonathan Miller
bf883a0770 Fix: replace shivammathur/setup-php with apt-get [skip ci] 2026-04-16 23:05:47 -05:00
Jonathan Miller
e24e712e7e Convert all workflows to Gitea-primary, GitHub backup only [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:44:55 -05:00
Jonathan Miller
eed04e417f Fix release.yml: GA_TOKEN for Gitea, GH_TOKEN for GitHub mirror only
Some checks failed
CodeQL Security Scanning / Analyze (javascript) (push) Failing after 39s
Repo Health / Access control (push) Failing after 1s
CodeQL Security Scanning / Analyze (actions) (push) Failing after 50s
Standards Compliance / Secret Scanning (push) Failing after 27s
Standards Compliance / License Header Validation (push) Failing after 39s
Standards Compliance / Repository Structure Validation (push) Failing after 37s
Standards Compliance / Coding Standards Check (push) Failing after 34s
Standards Compliance / Version Consistency Check (push) Failing after 33s
Standards Compliance / Workflow Configuration Check (push) Failing after 26s
Standards Compliance / Documentation Quality Check (push) Failing after 37s
Standards Compliance / README Completeness Check (push) Failing after 37s
Standards Compliance / Git Repository Hygiene (push) Failing after 32s
Standards Compliance / Script Integrity Validation (push) Failing after 28s
Standards Compliance / Line Length Check (push) Failing after 26s
Standards Compliance / File Naming Standards (push) Failing after 28s
Standards Compliance / Insecure Code Pattern Detection (push) Failing after 31s
Standards Compliance / Code Complexity Analysis (push) Failing after 28s
Standards Compliance / Dead Code Detection (push) Failing after 24s
Standards Compliance / Code Duplication Detection (push) Failing after 36s
Standards Compliance / Binary File Detection (push) Failing after 22s
Standards Compliance / File Size Limits (push) Failing after 35s
Standards Compliance / TODO/FIXME Tracking (push) Failing after 27s
Standards Compliance / Dependency Vulnerability Scanning (push) Failing after 33s
Standards Compliance / Unused Dependencies Check (push) Failing after 34s
Standards Compliance / Broken Link Detection (push) Failing after 38s
Standards Compliance / API Documentation Coverage (push) Failing after 34s
Standards Compliance / Accessibility Check (push) Failing after 27s
Standards Compliance / Performance Metrics (push) Failing after 33s
Standards Compliance / Enterprise Readiness Check (push) Failing after 37s
Repo Health / Release configuration (push) Has been skipped
Repo Health / Scripts governance (push) Has been skipped
Repo Health / Repository health (push) Has been skipped
CodeQL Security Scanning / Security Scan Summary (push) Successful in 1s
Standards Compliance / Terraform Configuration Validation (push) Successful in 1m7s
Standards Compliance / Repository Health Check (push) Failing after 11m54s
Standards Compliance / Compliance Summary (push) Has been cancelled
- Gitea API calls use secrets.GA_TOKEN
- GitHub mirror only for stable/rc, uses secrets.GH_TOKEN
- updates.xml now updates only the specific stability channel
  (version, SHA, date, download URLs) via Python regex

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:28:53 -05:00
Jonathan Miller
2862d2530e Rewrite release.yml: Gitea-primary, GitHub backup mirror
Some checks failed
CodeQL Security Scanning / Analyze (actions) (push) Has been cancelled
CodeQL Security Scanning / Analyze (javascript) (push) Has been cancelled
CodeQL Security Scanning / Security Scan Summary (push) Has been cancelled
Repo Health / Access control (push) Has been cancelled
Repo Health / Release configuration (push) Has been cancelled
Repo Health / Scripts governance (push) Has been cancelled
Repo Health / Repository health (push) Has been cancelled
Standards Compliance / Secret Scanning (push) Has been cancelled
Standards Compliance / License Header Validation (push) Has been cancelled
Standards Compliance / Repository Structure Validation (push) Has been cancelled
Standards Compliance / Coding Standards Check (push) Has been cancelled
Standards Compliance / Version Consistency Check (push) Has been cancelled
Standards Compliance / Workflow Configuration Check (push) Has been cancelled
Standards Compliance / Documentation Quality Check (push) Has been cancelled
Standards Compliance / README Completeness Check (push) Has been cancelled
Standards Compliance / Git Repository Hygiene (push) Has been cancelled
Standards Compliance / Script Integrity Validation (push) Has been cancelled
Standards Compliance / Line Length Check (push) Has been cancelled
Standards Compliance / File Naming Standards (push) Has been cancelled
Standards Compliance / Insecure Code Pattern Detection (push) Has been cancelled
Standards Compliance / Code Complexity Analysis (push) Has been cancelled
Standards Compliance / Code Duplication Detection (push) Has been cancelled
Standards Compliance / Dead Code Detection (push) Has been cancelled
Standards Compliance / File Size Limits (push) Has been cancelled
Standards Compliance / Binary File Detection (push) Has been cancelled
Standards Compliance / TODO/FIXME Tracking (push) Has been cancelled
Standards Compliance / Dependency Vulnerability Scanning (push) Has been cancelled
Standards Compliance / Unused Dependencies Check (push) Has been cancelled
Standards Compliance / Broken Link Detection (push) Has been cancelled
Standards Compliance / API Documentation Coverage (push) Has been cancelled
Standards Compliance / Accessibility Check (push) Has been cancelled
Standards Compliance / Performance Metrics (push) Has been cancelled
Standards Compliance / Enterprise Readiness Check (push) Has been cancelled
Standards Compliance / Repository Health Check (push) Has been cancelled
Standards Compliance / Terraform Configuration Validation (push) Has been cancelled
Standards Compliance / Compliance Summary (push) Has been cancelled
- All release creation via Gitea API (curl), no gh CLI or softprops
- GitHub mirror is optional (uses GH_MIRROR_TOKEN, continue-on-error)
- Stability input: development/alpha/beta/rc/stable with proper suffixes
- ZIP excludes sftp-config, .local, .env, keys
- Auto-updates updates.xml SHA-256 for matching channel
- Uses GITEA_TOKEN secret for authentication

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 22:09:54 -05:00
Jonathan Miller
7969dd1282 Migrate auto-release workflow to MokoStandards-API [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 19:15:27 -05:00
9ec2728796 chore: add mokoconsulting-tech/enterprise dependency
Some checks failed
CodeQL Security Scanning / Analyze (actions) (push) Failing after 40s
Repo Health / Access control (push) Failing after 1s
CodeQL Security Scanning / Analyze (javascript) (push) Failing after 45s
Standards Compliance / License Header Validation (push) Failing after 35s
Standards Compliance / Secret Scanning (push) Failing after 37s
Standards Compliance / Repository Structure Validation (push) Failing after 27s
Standards Compliance / Coding Standards Check (push) Failing after 26s
Standards Compliance / Version Consistency Check (push) Failing after 22s
Standards Compliance / Workflow Configuration Check (push) Failing after 30s
Standards Compliance / Documentation Quality Check (push) Failing after 31s
Standards Compliance / README Completeness Check (push) Failing after 26s
Standards Compliance / Git Repository Hygiene (push) Failing after 32s
Standards Compliance / Script Integrity Validation (push) Failing after 31s
Standards Compliance / Line Length Check (push) Failing after 31s
Standards Compliance / File Naming Standards (push) Failing after 33s
Standards Compliance / Insecure Code Pattern Detection (push) Failing after 31s
Standards Compliance / Code Complexity Analysis (push) Failing after 32s
Standards Compliance / Code Duplication Detection (push) Failing after 31s
Standards Compliance / Dead Code Detection (push) Failing after 30s
Standards Compliance / File Size Limits (push) Failing after 29s
Standards Compliance / Binary File Detection (push) Failing after 31s
Standards Compliance / TODO/FIXME Tracking (push) Failing after 26s
Standards Compliance / Dependency Vulnerability Scanning (push) Failing after 36s
Standards Compliance / Unused Dependencies Check (push) Failing after 28s
Standards Compliance / Broken Link Detection (push) Failing after 25s
Standards Compliance / API Documentation Coverage (push) Failing after 33s
Standards Compliance / Accessibility Check (push) Failing after 35s
Standards Compliance / Performance Metrics (push) Failing after 30s
Standards Compliance / Enterprise Readiness Check (push) Failing after 29s
Standards Compliance / Repository Health Check (push) Failing after 31s
Standards Compliance / Terraform Configuration Validation (push) Failing after 32s
CodeQL Security Scanning / Security Scan Summary (push) Has been cancelled
Repo Health / Release configuration (push) Has been cancelled
Repo Health / Scripts governance (push) Has been cancelled
Repo Health / Repository health (push) Has been cancelled
Standards Compliance / Compliance Summary (push) Has been cancelled
Auto-Assign Issues & PRs / Assign unassigned issues and PRs (push) Successful in 1s
2026-04-16 23:52:36 +00:00
Jonathan Miller
7b73aad3f8 chore: update dev SHA-256 for 03.09.16 [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:48:11 -05:00
Jonathan Miller
d01db8bc0d chore: update dev SHA-256 for 03.09.16 [skip ci]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 17:19:49 -05:00
8eb0d2e106 chore: sync updates.xml v03.09.15 [skip ci] 2026-04-15 02:08:54 +00:00
0a53acbed5 chore: sync updates.xml � fix GitHub URLs + dev SHA [skip ci] 2026-04-15 01:59:32 +00:00
47 changed files with 7464 additions and 4703 deletions

17
.gitattributes vendored
View File

@@ -1,17 +0,0 @@
# Force LF line endings for all text files
* text=auto eol=lf
# Explicitly mark binary files
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.svg text eol=lf
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.zip binary
*.gz binary
*.tar binary

42
.github/CLAUDE.md vendored
View File

@@ -115,34 +115,38 @@ BRIEF: One-line description
**`README.md` is the single source of truth for the repository version.** **`README.md` is the single source of truth for the repository version.**
- **Patch version is auto-bumped by the release workflow** — `release.yml` reads the current version from `README.md`, increments the patch (`XX.YY.ZZ``XX.YY.(ZZ+1)`), updates `README.md`, `templateDetails.xml`, and the matching channel in `updates.xml`, commits, pushes, then builds the ZIP. Manual bumping is no longer required. - **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03``01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it automatically to all badges and `FILE INFORMATION` headers on merge to `main`.
- The `VERSION: XX.YY.ZZ` field in `README.md` governs all other version references. - The `VERSION: XX.YY.ZZ` field in `README.md` governs all other version references.
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`). - Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`).
- Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only. - Never hardcode a specific version in document body text — use the badge or FILE INFORMATION header only.
### Joomla Version Alignment ### Joomla Version Alignment
The version in `README.md` **must always match** the `<version>` tag in `templateDetails.xml` and the matching channel entry in `updates.xml`. The release workflow updates all three automatically. The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
### Multi-Channel updates.xml
`updates.xml` contains separate `<update>` blocks per stability channel (development, alpha, beta, rc, stable). Each release workflow only modifies its own channel using targeted Python regex replacement — other channels are preserved untouched. Joomla filters by the user's "Minimum Stability" setting.
```xml ```xml
<!-- In manifest.xml — must match README.md version -->
<version>01.02.04</version>
<!-- In updates.xml — prepend a new <update> block for every release.
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
in the XML attribute value. Joomla's update server treats the value as a
regular expression, so \. matches a literal dot. -->
<updates> <updates>
<!-- 1. DEVELOPMENT --> <update>...<tag>development</tag>...</update> <update>
<!-- 2. ALPHA --> <update>...<tag>alpha</tag>...</update> <name>{{EXTENSION_NAME}}</name>
<!-- 3. BETA --> <update>...<tag>beta</tag>...</update> <version>01.02.04</version>
<!-- 4. RC --> <update>...<tag>rc</tag>...</update> <downloads>
<!-- 5. STABLE --> <update>...<tag>stable</tag>...</update> <downloadurl type="full" format="zip">
https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="4\.[0-9]+" />
</update>
<!-- … older entries preserved below … -->
</updates> </updates>
``` ```
**Key rules:**
- SHA-256 must be raw hex (no `sha256:` prefix)
- Version format must be `XX.YY.ZZ`, not tag names like `v01`
- Download URLs must point to Gitea (not GitHub) for all pre-release channels
--- ---
## Joomla Extension Structure ## Joomla Extension Structure
@@ -282,11 +286,11 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
| Change type | Documentation to update | | Change type | Documentation to update |
|-------------|------------------------| |-------------|------------------------|
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry | | New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
| New or changed manifest.xml | Release workflow auto-bumps version across README.md, templateDetails.xml, and updates.xml | | New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
| New release | Trigger `release.yml` — auto-bumps patch, builds ZIP, updates matching channel in `updates.xml` | | New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
| New or changed workflow | `docs/workflows/<workflow-name>.md` | | New or changed workflow | `docs/workflows/<workflow-name>.md` |
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block | | Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
| **Every release** | **Patch auto-bumped** by `release.yml` — no manual version bump needed | | **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
--- ---

View File

@@ -7,7 +7,7 @@
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards # REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /.github/workflows/auto-assign.yml # PATH: /.github/workflows/auto-assign.yml
# VERSION: 04.06.00 # VERSION: 04.06.00
# BRIEF: Auto-assign jmiller to unassigned issues and PRs every 15 minutes # BRIEF: Auto-assign jmiller-moko to unassigned issues and PRs every 15 minutes
name: Auto-Assign Issues & PRs name: Auto-Assign Issues & PRs
@@ -35,7 +35,7 @@ jobs:
GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }} GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
run: | run: |
REPO="${{ github.repository }}" REPO="${{ github.repository }}"
ASSIGNEE="jmiller" ASSIGNEE="jmiller-moko"
echo "## 🏷️ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY echo "## 🏷️ Auto-Assign Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY

View File

@@ -86,7 +86,7 @@ jobs:
# Check for existing issue with same title prefix # Check for existing issue with same title prefix
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=open&per_page=10" 2>/dev/null \ EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=open&per_page=10" 2>/dev/null \
| jq -r ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1) --jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}(${VERSION})\")) | .number" 2>/dev/null | head -1)
if [ -n "$EXISTING" ]; then if [ -n "$EXISTING" ]; then
echo " Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY echo " Issue #${EXISTING} already exists for ${VERSION}" >> $GITHUB_STEP_SUMMARY
@@ -135,7 +135,7 @@ jobs:
--title "$SUB_FULL_TITLE" \ --title "$SUB_FULL_TITLE" \
--body "$SUB_BODY" \ --body "$SUB_BODY" \
--label "${SUB_LABELS}" \ --label "${SUB_LABELS}" \
--assignee "jmiller" 2>&1) --assignee "jmiller-moko" 2>&1)
SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$') SUB_NUM=$(echo "$SUB_URL" | grep -oE '[0-9]+$')
if [ -n "$SUB_NUM" ]; then if [ -n "$SUB_NUM" ]; then
@@ -154,7 +154,7 @@ jobs:
--title "$TITLE" \ --title "$TITLE" \
--body "$PARENT_BODY" \ --body "$PARENT_BODY" \
--label "${LABEL_TYPE},version" \ --label "${LABEL_TYPE},version" \
--assignee "jmiller" 2>&1) --assignee "jmiller-moko" 2>&1)
PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$') PARENT_NUM=$(echo "$PARENT_URL" | grep -oE '[0-9]+$')
@@ -164,7 +164,7 @@ jobs:
IFS='|' read -r SUB_TITLE _ _ <<< "$SUB" IFS='|' read -r SUB_TITLE _ _ <<< "$SUB"
SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}" SUB_FULL_TITLE="${TITLE_PREFIX}(${VERSION}): ${SUB_TITLE}"
SUB_NUM=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=open&per_page=20" 2>/dev/null \ SUB_NUM=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=open&per_page=20" 2>/dev/null \
| jq -r ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1) --jq ".[] | select(.title == \"${SUB_FULL_TITLE}\") | .number" 2>/dev/null | head -1)
if [ -n "$SUB_NUM" ]; then if [ -n "$SUB_NUM" ]; then
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${SUB_NUM}" 2>/dev/null -X PATCH \ curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${SUB_NUM}" 2>/dev/null -X PATCH \
-f body="$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${SUB_NUM}" | jq -r '.body' 2>/dev/null) -f body="$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${SUB_NUM}" | jq -r '.body' 2>/dev/null)

View File

@@ -26,7 +26,7 @@
# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml | # | 8. Build ZIP, upload asset, write SHA-256 to updates.xml |
# | | # | |
# | Every version change: archives main -> version/XX.YY branch | # | Every version change: archives main -> version/XX.YY branch |
# | All patches release (including 00). Patch 00/01 = full pipeline. | # | Patch 00 = development (no release). First release = patch 01. |
# | First release only (patch == 01): | # | First release only (patch == 01): |
# | 7b. Create new GitHub Release | # | 7b. Create new GitHub Release |
# | | # | |
@@ -53,7 +53,7 @@ permissions:
jobs: jobs:
release: release:
name: Build & Release Pipeline name: Build & Release Pipeline
runs-on: release runs-on: ubuntu-latest
if: >- if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
@@ -64,12 +64,9 @@ jobs:
token: ${{ secrets.GA_TOKEN || github.token }} token: ${{ secrets.GA_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Set authenticated push URL
run: git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Setup MokoStandards tools - name: Setup MokoStandards tools
env: env:
GA_TOKEN: ${{ secrets.GA_TOKEN || github.token }} GH_TOKEN: ${{ secrets.GA_TOKEN || github.token }}
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}' COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
run: | run: |
git clone --depth 1 --branch version/04 --quiet \ git clone --depth 1 --branch version/04 --quiet \
@@ -100,14 +97,20 @@ jobs:
echo "minor=$MINOR" >> "$GITHUB_OUTPUT" echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
echo "major=$MAJOR" >> "$GITHUB_OUTPUT" echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT" echo "release_tag=v${MAJOR}" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "00" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch 00 = development — skipping release)"
else
echo "skip=false" >> "$GITHUB_OUTPUT" echo "skip=false" >> "$GITHUB_OUTPUT"
if [ "$PATCH" = "00" ] || [ "$PATCH" = "01" ]; then if [ "$PATCH" = "01" ]; then
echo "is_minor=true" >> "$GITHUB_OUTPUT" echo "is_minor=true" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (first release for this minor — full pipeline)" echo "Version: $VERSION (first release — full pipeline)"
else else
echo "is_minor=false" >> "$GITHUB_OUTPUT" echo "is_minor=false" >> "$GITHUB_OUTPUT"
echo "Version: $VERSION (patch — platform version + badges only)" echo "Version: $VERSION (patch — platform version + badges only)"
fi fi
fi
- name: Check if already released - name: Check if already released
if: steps.version.outputs.skip != 'true' if: steps.version.outputs.skip != 'true'
@@ -514,9 +517,9 @@ jobs:
# Replace downloads block with both formats + SHA # Replace downloads block with both formats + SHA
sed -i "s|<downloads>.*</downloads>|<downloads>\n <downloadurl type=\"full\" format=\"zip\">${ZIP_URL}</downloadurl>\n <downloadurl type=\"full\" format=\"tar.gz\">${TAR_URL}</downloadurl>\n </downloads>|" updates.xml 2>/dev/null || true sed -i "s|<downloads>.*</downloads>|<downloads>\n <downloadurl type=\"full\" format=\"zip\">${ZIP_URL}</downloadurl>\n <downloadurl type=\"full\" format=\"tar.gz\">${TAR_URL}</downloadurl>\n </downloads>|" updates.xml 2>/dev/null || true
if grep -q '<sha256>' updates.xml; then if grep -q '<sha256>' updates.xml; then
sed -i "s|<sha256>.*</sha256>|<sha256>${SHA256_ZIP}</sha256>|" updates.xml sed -i "s|<sha256>.*</sha256>|<sha256>sha256:${SHA256_ZIP}</sha256>|" updates.xml
else else
sed -i "s|</downloads>|</downloads>\n <sha256>${SHA256_ZIP}</sha256>|" updates.xml sed -i "s|</downloads>|</downloads>\n <sha256>sha256:${SHA256_ZIP}</sha256>|" updates.xml
fi fi
git add updates.xml git add updates.xml

View File

@@ -70,60 +70,33 @@ jobs:
echo "sha256=${SHA256_HASH}" >> $GITHUB_OUTPUT echo "sha256=${SHA256_HASH}" >> $GITHUB_OUTPUT
echo "SHA-256 Hash: ${SHA256_HASH}" echo "SHA-256 Hash: ${SHA256_HASH}"
- name: Determine stability channel - name: Update updates.xml
id: channel
run: | run: |
TAG="${{ steps.tag.outputs.tag }}" TAG="${{ steps.tag.outputs.tag }}"
case "$TAG" in SHA256="${{ steps.sha.outputs.sha256 }}"
development) STABILITY="development" ;;
alpha) STABILITY="alpha" ;;
beta) STABILITY="beta" ;;
release-candidate) STABILITY="rc" ;;
*) STABILITY="stable" ;;
esac
echo "stability=${STABILITY}" >> $GITHUB_OUTPUT
echo "Channel: ${STABILITY}"
- name: Update updates.xml (targeted channel only)
env:
PY_TAG: ${{ steps.tag.outputs.tag }}
PY_SHA: ${{ steps.sha.outputs.sha256 }}
PY_STABILITY: ${{ steps.channel.outputs.stability }}
run: |
DATE=$(date +%Y-%m-%d) DATE=$(date +%Y-%m-%d)
export PY_DATE="$DATE"
python3 << 'PYEOF' # Update version
import re, os sed -i "s|<version>.*</version>|<version>${TAG}</version>|" updates.xml
tag = os.environ["PY_TAG"] # Update creation date
sha256 = os.environ["PY_SHA"] sed -i "s|<creationDate>.*</creationDate>|<creationDate>${DATE}</creationDate>|" updates.xml
date = os.environ["PY_DATE"]
stability = os.environ["PY_STABILITY"]
with open("updates.xml") as f: # Update download URL
content = f.read() sed -i "s|<downloadurl type='full' format='zip'>.*</downloadurl>|<downloadurl type='full' format='zip'>https://github.com/${{ github.repository }}/releases/download/${TAG}/mokocassiopeia-src-${TAG}.zip</downloadurl>|" updates.xml
pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(stability) + r"</tag>.*?</update>)" # Update or add SHA-256 hash
match = re.search(pattern, content, re.DOTALL) if grep -q "<sha256>" updates.xml; then
sed -i "s|<sha256>.*</sha256>|<sha256>sha256:${SHA256}</sha256>|" updates.xml
else
# Add SHA-256 after downloadurl
sed -i "/<\/downloadurl>/a\ <sha256>sha256:${SHA256}<\/sha256>" updates.xml
fi
if not match: echo "Updated updates.xml with:"
print(f"No <update> block for <tag>{stability}</tag> — skipping") echo " Version: ${TAG}"
exit(0) echo " Date: ${DATE}"
echo " SHA-256: ${SHA256}"
block = match.group(1)
original = block
block = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", block)
block = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", block)
content = content.replace(original, block)
with open("updates.xml", "w") as f:
f.write(content)
print(f"Updated {stability} channel: sha={sha256[:16]}..., date={date}")
PYEOF
- name: Check for changes - name: Check for changes
id: changes id: changes
@@ -145,10 +118,8 @@ jobs:
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]" git config --local user.name "gitea-actions[bot]"
STABILITY="${{ steps.channel.outputs.stability }}"
git add updates.xml git add updates.xml
git commit -m "chore: update ${STABILITY} SHA-256 for ${TAG} [skip ci]" \ git commit -m "chore: Update SHA-256 hash for release ${TAG} - SHA: ${{ steps.sha.outputs.sha256 }}"
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push origin main git push origin main

View File

@@ -46,7 +46,7 @@ jobs:
ACTOR="${{ github.actor }}" ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}" REPO="${{ github.repository }}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/collaborators/${ACTOR}/permission" 2>/dev/null \ PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/collaborators/${ACTOR}/permission" 2>/dev/null \
2>/dev/null | jq -r '.permission' || echo "read") --jq '.permission' 2>/dev/null || echo "read")
if [ "$PERMISSION" != "admin" ]; then if [ "$PERMISSION" != "admin" ]; then
echo "Denied: only admins can freeze/unfreeze branches (${ACTOR} has ${PERMISSION})" echo "Denied: only admins can freeze/unfreeze branches (${ACTOR} has ${PERMISSION})"
exit 1 exit 1
@@ -67,7 +67,7 @@ jobs:
if [ "$ACTION" = "freeze" ]; then if [ "$ACTION" = "freeze" ]; then
# Check if ruleset already exists # Check if ruleset already exists
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets" 2>/dev/null \ EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets" 2>/dev/null \
| jq -r ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true) --jq ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true)
if [ -n "$EXISTING" ]; then if [ -n "$EXISTING" ]; then
echo "Branch \`${BRANCH}\` is already frozen (ruleset #${EXISTING})" >> $GITHUB_STEP_SUMMARY echo "Branch \`${BRANCH}\` is already frozen (ruleset #${EXISTING})" >> $GITHUB_STEP_SUMMARY
@@ -80,7 +80,7 @@ jobs:
printf '"conditions":{"ref_name":{"include":["refs/heads/%s"],"exclude":[]}},' "${BRANCH}" >> /tmp/ruleset.json printf '"conditions":{"ref_name":{"include":["refs/heads/%s"],"exclude":[]}},' "${BRANCH}" >> /tmp/ruleset.json
printf '"rules":[{"type":"update"},{"type":"deletion"},{"type":"non_fast_forward"}]}' >> /tmp/ruleset.json printf '"rules":[{"type":"update"},{"type":"deletion"},{"type":"non_fast_forward"}]}' >> /tmp/ruleset.json
RESULT=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets" 2>/dev/null -X POST -d @/tmp/ruleset.json 2>&1 | jq -r '.id') || true RESULT=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets" 2>/dev/null -X POST --input /tmp/ruleset.json --jq '.id' 2>&1) || true
if echo "$RESULT" | grep -qE '^[0-9]+$'; then if echo "$RESULT" | grep -qE '^[0-9]+$'; then
echo "Frozen \`${BRANCH}\` — ruleset #${RESULT}" >> $GITHUB_STEP_SUMMARY echo "Frozen \`${BRANCH}\` — ruleset #${RESULT}" >> $GITHUB_STEP_SUMMARY
@@ -99,7 +99,7 @@ jobs:
elif [ "$ACTION" = "unfreeze" ]; then elif [ "$ACTION" = "unfreeze" ]; then
# Find and delete the freeze ruleset # Find and delete the freeze ruleset
RULESET_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets" 2>/dev/null \ RULESET_ID=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/rulesets" 2>/dev/null \
| jq -r ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true) --jq ".[] | select(.name == \"${RULESET_NAME}\") | .id" 2>/dev/null || true)
if [ -z "$RULESET_ID" ]; then if [ -z "$RULESET_ID" ]; then
echo "Branch \`${BRANCH}\` is not frozen (no ruleset found)" >> $GITHUB_STEP_SUMMARY echo "Branch \`${BRANCH}\` is not frozen (no ruleset found)" >> $GITHUB_STEP_SUMMARY

View File

@@ -32,7 +32,7 @@ permissions:
jobs: jobs:
deploy: deploy:
name: SFTP Deploy to Dev name: SFTP Deploy to Dev
runs-on: release runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository

View File

@@ -47,7 +47,7 @@ env:
jobs: jobs:
build: build:
name: Build Release Package name: Build Release Package
runs-on: release runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -95,95 +95,6 @@ jobs:
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT" echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "Building: ${ZIP_NAME} (${STABILITY})" echo "Building: ${ZIP_NAME} (${STABILITY})"
- name: Auto-bump patch version
id: bump
env:
GA_TOKEN: ${{ secrets.GA_TOKEN }}
INPUT_VERSION: ${{ steps.meta.outputs.version }}
INPUT_STABILITY: ${{ steps.meta.outputs.stability }}
INPUT_SUFFIX: ${{ steps.meta.outputs.suffix }}
run: |
BRANCH="${{ github.ref_name }}"
GITEA_API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
# Read current version from README.md
CURRENT=$(sed -n 's/.*VERSION:[[:space:]]*\([0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]\).*/\1/p' README.md 2>/dev/null | head -1)
if [ -z "$CURRENT" ]; then
echo "No VERSION in README.md — using input version"
echo "version=${INPUT_VERSION}" >> "$GITHUB_OUTPUT"
echo "zip_name=${EXT_ELEMENT}-${INPUT_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
exit 0
fi
# Bump patch: XX.YY.ZZ → XX.YY.(ZZ+1)
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
MINOR=$(echo "$CURRENT" | cut -d. -f2)
PATCH=$(echo "$CURRENT" | cut -d. -f3)
NEW_PATCH=$(printf "%02d" $((10#$PATCH + 1)))
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
echo "Bumping: ${CURRENT} → ${NEW_VERSION}"
# Update README.md
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${NEW_VERSION}/" README.md
# Update templateDetails.xml / manifest
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
sed -i "s|<version>${CURRENT}</version>|<version>${NEW_VERSION}</version>|" "$MANIFEST"
fi
# Update only the matching stability channel in updates.xml
if [ -f "updates.xml" ]; then
export PY_OLD="$CURRENT" PY_NEW="$NEW_VERSION" PY_STABILITY="$INPUT_STABILITY"
python3 << 'PYEOF'
import re, os
old = os.environ["PY_OLD"]
new = os.environ["PY_NEW"]
stability = os.environ["PY_STABILITY"]
with open("updates.xml") as f:
content = f.read()
pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(stability) + r"</tag>.*?</update>)"
match = re.search(pattern, content, re.DOTALL)
if match:
block = match.group(1)
updated = block.replace(old, new)
content = content.replace(block, updated)
with open("updates.xml", "w") as f:
f.write(content)
print(f"Updated {stability} channel: {old} -> {new}")
PYEOF
fi
# Commit bump to current branch
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://jmiller:${GA_TOKEN}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): bump ${CURRENT} → ${NEW_VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
# For stable releases from dev: merge dev → main via Gitea API
if [ "$INPUT_STABILITY" = "stable" ] && [ "$BRANCH" != "main" ]; then
echo "Merging ${BRANCH} → main via Gitea API..."
MERGE_RESULT=$(curl -sf -X POST -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${GITEA_API}/merges" \
-d "$(jq -n \
--arg base "main" \
--arg head "${BRANCH}" \
--arg msg "chore(release): merge ${BRANCH} for stable ${NEW_VERSION} [skip ci]" \
'{base: $base, head: $head, merge_message_field: $msg}'
)" 2>&1) || true
echo "Merge result: ${MERGE_RESULT}"
fi
echo "version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
echo "zip_name=${EXT_ELEMENT}-${NEW_VERSION}${INPUT_SUFFIX}.zip" >> "$GITHUB_OUTPUT"
- name: Install dependencies - name: Install dependencies
env: env:
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}' COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.GA_TOKEN }}"}}}'
@@ -208,7 +119,7 @@ jobs:
- name: Build ZIP - name: Build ZIP
id: zip id: zip
run: | run: |
ZIP_NAME="${{ steps.bump.outputs.zip_name }}" ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
cd build/package cd build/package
zip -r "../${ZIP_NAME}" . zip -r "../${ZIP_NAME}" .
cd .. cd ..
@@ -228,8 +139,8 @@ jobs:
TOKEN="${{ secrets.GA_TOKEN }}" TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Find and delete existing release by tag (may not exist — ignore 404) # Find and delete existing release by tag
RELEASE_ID=$(curl -s -H "Authorization: token ${TOKEN}" \ RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty') "${API}/releases/tags/${TAG}" 2>/dev/null | jq -r '.id // empty')
if [ -n "$RELEASE_ID" ]; then if [ -n "$RELEASE_ID" ]; then
@@ -246,7 +157,7 @@ jobs:
id: gitea_release id: gitea_release
run: | run: |
TAG="${{ steps.meta.outputs.tag_name }}" TAG="${{ steps.meta.outputs.tag_name }}"
VERSION="${{ steps.bump.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}" STABILITY="${{ steps.meta.outputs.stability }}"
PRERELEASE="${{ steps.meta.outputs.prerelease }}" PRERELEASE="${{ steps.meta.outputs.prerelease }}"
SHA256="${{ steps.zip.outputs.sha256 }}" SHA256="${{ steps.zip.outputs.sha256 }}"
@@ -296,7 +207,7 @@ jobs:
- name: "Gitea: Upload ZIP" - name: "Gitea: Upload ZIP"
run: | run: |
RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}" RELEASE_ID="${{ steps.gitea_release.outputs.release_id }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}" ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
TOKEN="${{ secrets.GA_TOKEN }}" TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
@@ -314,9 +225,9 @@ jobs:
continue-on-error: true continue-on-error: true
run: | run: |
TAG="${{ steps.meta.outputs.tag_name }}" TAG="${{ steps.meta.outputs.tag_name }}"
VERSION="${{ steps.bump.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}" STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}" ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.zip.outputs.sha256 }}" SHA256="${{ steps.zip.outputs.sha256 }}"
TOKEN="${{ secrets.GH_TOKEN }}" TOKEN="${{ secrets.GH_TOKEN }}"
GH_REPO="mokoconsulting-tech/${GITEA_REPO}" GH_REPO="mokoconsulting-tech/${GITEA_REPO}"
@@ -347,7 +258,7 @@ jobs:
--arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^} (mirror)" \ --arg name "${EXT_ELEMENT} ${VERSION} ${STABILITY^} (mirror)" \
--arg body "Mirror of Gitea release. SHA-256: \`${SHA256}\`" \ --arg body "Mirror of Gitea release. SHA-256: \`${SHA256}\`" \
--argjson pre "$IS_PRE" \ --argjson pre "$IS_PRE" \
'{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre, draft: false}' '{tag_name: $tag, target_commitish: $target, name: $name, body: $body, prerelease: $pre}'
)" | jq -r '.id') )" | jq -r '.id')
# Upload ZIP # Upload ZIP
@@ -364,9 +275,9 @@ jobs:
- name: "Update updates.xml for this channel" - name: "Update updates.xml for this channel"
run: | run: |
STABILITY="${{ steps.meta.outputs.stability }}" STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.bump.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
SHA256="${{ steps.zip.outputs.sha256 }}" SHA256="${{ steps.zip.outputs.sha256 }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}" ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
TAG="${{ steps.meta.outputs.tag_name }}" TAG="${{ steps.meta.outputs.tag_name }}"
DATE=$(date +%Y-%m-%d) DATE=$(date +%Y-%m-%d)
@@ -375,7 +286,6 @@ jobs:
exit 0 exit 0
fi fi
# Cascading channels: each stability updates itself and all lower levels
export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \ export PY_STABILITY="$STABILITY" PY_VERSION="$VERSION" PY_SHA256="$SHA256" \
PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \ PY_ZIP_NAME="$ZIP_NAME" PY_TAG="$TAG" PY_DATE="$DATE" \
PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO" PY_GITEA_ORG="$GITEA_ORG" PY_GITEA_REPO="$GITEA_REPO"
@@ -391,107 +301,90 @@ jobs:
gitea_org = os.environ["PY_GITEA_ORG"] gitea_org = os.environ["PY_GITEA_ORG"]
gitea_repo = os.environ["PY_GITEA_REPO"] gitea_repo = os.environ["PY_GITEA_REPO"]
# Cascade map: each level updates itself + all lower levels # Map stability to the <tag> value in updates.xml
cascade = { tag_map = {
"stable": ["development", "alpha", "beta", "rc", "stable"], "development": "development",
"rc": ["development", "alpha", "beta", "rc"], "alpha": "alpha",
"beta": ["development", "alpha", "beta"], "beta": "beta",
"alpha": ["development", "alpha"], "rc": "rc",
"development": ["development"], "stable": "stable",
} }
targets = cascade.get(stability, [stability]) xml_tag = tag_map.get(stability, "development")
with open("updates.xml", "r") as f: with open("updates.xml", "r") as f:
content = f.read() content = f.read()
gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}" # Build regex to find the <update> block containing this stability tag
# Match from <update> to </update> that contains <tag>xml_tag</tag>
for xml_tag in targets: block_pattern = r"(<update>.*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
block_pattern = r"(<update>(?:(?!</update>).)*?<tag>" + re.escape(xml_tag) + r"</tag>.*?</update>)"
match = re.search(block_pattern, content, re.DOTALL) match = re.search(block_pattern, content, re.DOTALL)
if not match: if not match:
print(f"No block for <tag>{xml_tag}</tag> — skipping") print(f"No <update> block found for <tag>{xml_tag}</tag>")
continue exit(0)
block = match.group(1) block = match.group(1)
original = block original_block = block
# Update version
block = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block) block = re.sub(r"<version>[^<]*</version>", f"<version>{version}</version>", block)
# Update creation date
block = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", block) block = re.sub(r"<creationDate>[^<]*</creationDate>", f"<creationDate>{date}</creationDate>", block)
if "<sha256>" in block: # Update SHA-256
block = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>{sha256}</sha256>", block) block = re.sub(r"<sha256>[^<]*</sha256>", f"<sha256>sha256:{sha256}</sha256>", block)
else:
block = block.replace("</downloads>", f"</downloads>\n <sha256>{sha256}</sha256>")
# Update Gitea download URL
gitea_url = f"https://git.mokoconsulting.tech/{gitea_org}/{gitea_repo}/releases/download/{tag}/{zip_name}"
block = re.sub( block = re.sub(
r"(<downloadurl[^>]*>)https://git\.mokoconsulting\.tech/[^<]*(</downloadurl>)", r"(<downloadurl[^>]*>)https://git\.mokoconsulting\.tech/[^<]*(</downloadurl>)",
rf"\g<1>{gitea_url}\g<2>", rf"\g<1>{gitea_url}\g<2>",
block block
) )
content = content.replace(original, block) # Update GitHub download URL only for RC and stable (others are Gitea-only)
print(f"Updated {xml_tag} channel") if stability in ("rc", "stable"):
gh_url = f"https://github.com/mokoconsulting-tech/{gitea_repo}/releases/download/{tag}/{zip_name}"
block = re.sub(
r"(<downloadurl[^>]*>)https://github\.com/[^<]*(</downloadurl>)",
rf"\g<1>{gh_url}\g<2>",
block
)
else:
# Remove any GitHub download URL for dev/alpha/beta
block = re.sub(
r"\n\s*<downloadurl[^>]*>https://github\.com/[^<]*</downloadurl>",
"",
block
)
content = content.replace(original_block, block)
with open("updates.xml", "w") as f: with open("updates.xml", "w") as f:
f.write(content) f.write(content)
print(f"Cascaded {stability} → {', '.join(targets)}: v={version}, sha={sha256[:16]}...") print(f"Updated {xml_tag} channel: version={version}, sha={sha256[:16]}..., date={date}")
PYEOF PYEOF
- name: "Commit updates.xml to current branch and main" - name: "Commit updates.xml"
run: | run: |
if git diff --quiet updates.xml 2>/dev/null; then if git diff --quiet updates.xml 2>/dev/null; then
echo "No changes to updates.xml" echo "No changes to updates.xml"
exit 0 exit 0
fi fi
STABILITY="${{ steps.meta.outputs.stability }}"
VERSION="${{ steps.bump.outputs.version }}"
CURRENT_BRANCH="${{ github.ref_name }}"
TOKEN="${{ secrets.GA_TOKEN }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech" git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]" git config --local user.name "gitea-actions[bot]"
git add updates.xml git add updates.xml
git commit -m "chore: update ${STABILITY} SHA-256 for ${VERSION} [skip ci]" \ git commit -m "chore: update ${STABILITY} SHA-256 for ${{ steps.meta.outputs.version }} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" --author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
# Set push URL with GA_TOKEN for authenticated pushes (branch protection requires jmiller)
git remote set-url origin "https://jmiller:${{ secrets.GA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
# Push to current branch
git push || true git push || true
# Sync updates.xml to main via direct API
GA_TOKEN="${{ secrets.GA_TOKEN }}"
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GA_TOKEN}" \
"${API}/contents/updates.xml?ref=main" | jq -r '.sha // empty')
if [ -n "$FILE_SHA" ]; then
CONTENT=$(base64 -w0 updates.xml)
curl -sf -X PUT -H "Authorization: token ${GA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/contents/updates.xml" \
-d "$(jq -n \
--arg content "$CONTENT" \
--arg sha "$FILE_SHA" \
--arg msg "chore: sync updates.xml ${STABILITY} ${VERSION} [skip ci]" \
--arg branch "main" \
'{content: $content, sha: $sha, message: $msg, branch: $branch}'
)" > /dev/null 2>&1 \
&& echo "updates.xml synced to main" \
|| echo "WARNING: failed to sync updates.xml to main"
fi
- name: Summary - name: Summary
run: | run: |
VERSION="${{ steps.bump.outputs.version }}" VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}" STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.bump.outputs.zip_name }}" ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.zip.outputs.sha256 }}" SHA256="${{ steps.zip.outputs.sha256 }}"
TAG="${{ steps.meta.outputs.tag_name }}" TAG="${{ steps.meta.outputs.tag_name }}"

View File

@@ -87,54 +87,62 @@ jobs:
steps: steps:
- name: Check actor permission (admin only) - name: Check actor permission (admin only)
id: perm id: perm
run: | uses: actions/github-script@v7
ACTOR="${{ github.actor }}" with:
REPO="${{ github.repository }}" github-token: ${{ secrets.GH_TOKEN }}
TOKEN="${{ secrets.GA_TOKEN }}" script: |
GITEA_API="${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1" const actor = context.actor;
let permission = "unknown";
let allowed = false;
let method = "";
PERMISSION="unknown" // Hardcoded authorized users — always allowed
ALLOWED="false" const authorizedUsers = ["jmiller-moko", "gitea-actions[bot]"];
METHOD="" if (authorizedUsers.includes(actor)) {
allowed = true;
permission = "admin";
method = "hardcoded allowlist";
} else {
// Check via API for other actors
try {
const res = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: actor,
});
permission = (res?.data?.permission || "unknown").toLowerCase();
allowed = permission === "admin" || permission === "maintain";
method = "repo collaborator API";
} catch (error) {
core.warning(`Could not fetch permissions for '${actor}': ${error.message}`);
permission = "unknown";
allowed = false;
method = "API error";
}
}
# Hardcoded authorized users core.setOutput("permission", permission);
if [ "$ACTOR" = "jmiller" ] || [ "$ACTOR" = "gitea-actions[bot]" ]; then core.setOutput("allowed", allowed ? "true" : "false");
PERMISSION="admin"
ALLOWED="true"
METHOD="hardcoded allowlist"
else
# Check via Gitea API
RESULT=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${GITEA_API}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
PERMISSION=$(echo "$RESULT" | jq -r '.permission // "unknown"')
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "owner" ] || [ "$PERMISSION" = "maintain" ]; then
ALLOWED="true"
fi
METHOD="Gitea collaborator API"
fi
echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT" const lines = [
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT" "## 🔐 Access Authorization",
"",
"| Field | Value |",
"|-------|-------|",
`| **Actor** | \`${actor}\` |`,
`| **Repository** | \`${context.repo.owner}/${context.repo.repo}\` |`,
`| **Permission** | \`${permission}\` |`,
`| **Method** | ${method} |`,
`| **Authorized** | ${allowed} |`,
`| **Trigger** | \`${context.eventName}\` |`,
`| **Branch** | \`${context.ref.replace('refs/heads/', '')}\` |`,
"",
allowed
? `✅ ${actor} authorized (${method})`
: `❌ ${actor} is NOT authorized. Requires admin or maintain role, or be in the hardcoded allowlist.`,
];
{ await core.summary.addRaw(lines.join("\n")).write();
echo "## 🔐 Access Authorization"
echo ""
echo "| Field | Value |"
echo "|-------|-------|"
echo "| **Actor** | \`${ACTOR}\` |"
echo "| **Repository** | \`${REPO}\` |"
echo "| **Permission** | \`${PERMISSION}\` |"
echo "| **Method** | ${METHOD} |"
echo "| **Authorized** | ${ALLOWED} |"
echo "| **Trigger** | \`${{ github.event_name }}\` |"
echo "| **Branch** | \`${GITHUB_REF#refs/heads/}\` |"
echo ""
if [ "$ALLOWED" = "true" ]; then
echo "✅ ${ACTOR} authorized (${METHOD})"
else
echo "❌ ${ACTOR} is NOT authorized. Requires admin or maintain role."
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Deny execution when not permitted - name: Deny execution when not permitted
if: ${{ steps.perm.outputs.allowed != 'true' }} if: ${{ steps.perm.outputs.allowed != 'true' }}

View File

@@ -80,7 +80,7 @@ jobs:
echo "✅ Scheduled run — authorized" echo "✅ Scheduled run — authorized"
exit 0 exit 0
fi fi
AUTHORIZED_USERS="jmiller gitea-actions[bot]" AUTHORIZED_USERS="jmiller-moko gitea-actions[bot]"
for user in $AUTHORIZED_USERS; do for user in $AUTHORIZED_USERS; do
if [ "$ACTOR" = "$user" ]; then if [ "$ACTOR" = "$user" ]; then
echo "✅ ${ACTOR} authorized" echo "✅ ${ACTOR} authorized"
@@ -88,7 +88,7 @@ jobs:
fi fi
done done
PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${ACTOR}/permission" \ PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${ACTOR}/permission" \
2>/dev/null | jq -r '.permission') --jq '.permission' 2>/dev/null)
case "$PERMISSION" in case "$PERMISSION" in
admin|maintain) echo "✅ ${ACTOR} has ${PERMISSION}" ;; admin|maintain) echo "✅ ${ACTOR} has ${PERMISSION}" ;;
*) echo "❌ Admin or maintain required"; exit 1 ;; *) echo "❌ Admin or maintain required"; exit 1 ;;
@@ -191,7 +191,7 @@ jobs:
echo "## 🏷️ Label Reset" >> $GITHUB_STEP_SUMMARY echo "## 🏷️ Label Reset" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/labels?per_page=100" 2>/dev/null | jq -r '.[].name' | while read -r label; do curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/labels?per_page=100" 2>/dev/null --paginate --jq '.[].name' | while read -r label; do
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$label', safe=''))") ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$label', safe=''))")
gh api -X DELETE "repos/${REPO}/labels/${ENCODED}" --silent 2>/dev/null || true gh api -X DELETE "repos/${REPO}/labels/${ENCODED}" --silent 2>/dev/null || true
done done
@@ -278,7 +278,7 @@ jobs:
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/branches?per_page=100" | jq -r '.[].name' 2>/dev/null | \ curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/branches?per_page=100" | jq -r '.[].name' 2>/dev/null | \
grep "^chore/sync-mokostandards" | \ grep "^chore/sync-mokostandards" | \
grep -v "^${CURRENT}$" | while read -r branch; do grep -v "^${CURRENT}$" | while read -r branch; do
gh pr list --repo "$REPO" --head "$branch" --state open --json number 2>/dev/null | jq -r '.[].number' | while read -r pr; do gh pr list --repo "$REPO" --head "$branch" --state open --json number --jq '.[].number' 2>/dev/null | while read -r pr; do
gh pr close "$pr" --repo "$REPO" --comment "Superseded by \`${CURRENT}\`" 2>/dev/null || true gh pr close "$pr" --repo "$REPO" --comment "Superseded by \`${CURRENT}\`" 2>/dev/null || true
echo " Closed PR #${pr}" >> $GITHUB_STEP_SUMMARY echo " Closed PR #${pr}" >> $GITHUB_STEP_SUMMARY
done done
@@ -305,7 +305,7 @@ jobs:
# Delete cancelled and stale workflow runs # Delete cancelled and stale workflow runs
for status in cancelled stale; do for status in cancelled stale; do
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/actions/runs?status=${status}&per_page=100" 2>/dev/null \ curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/actions/runs?status=${status}&per_page=100" 2>/dev/null \
2>/dev/null | jq -r '.workflow_runs[].id' | while read -r run_id; do --jq '.workflow_runs[].id' 2>/dev/null | while read -r run_id; do
gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}" --silent 2>/dev/null || true gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}" --silent 2>/dev/null || true
DELETED=$((DELETED+1)) DELETED=$((DELETED+1))
done done
@@ -327,7 +327,7 @@ jobs:
DELETED=0 DELETED=0
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/actions/runs?created=<${CUTOFF}&per_page=100" 2>/dev/null \ curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/actions/runs?created=<${CUTOFF}&per_page=100" 2>/dev/null \
2>/dev/null | jq -r '.workflow_runs[].id' | while read -r run_id; do --jq '.workflow_runs[].id' 2>/dev/null | while read -r run_id; do
gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}/logs" --silent 2>/dev/null || true gh api -X DELETE "repos/${REPO}/actions/runs/${run_id}/logs" --silent 2>/dev/null || true
DELETED=$((DELETED+1)) DELETED=$((DELETED+1))
done done
@@ -504,7 +504,7 @@ jobs:
DELETED=0 DELETED=0
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=closed&since=1970-01-01T00:00:00Z&per_page=100&sort=updated&direction=asc" 2>/dev/null \ curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?state=closed&since=1970-01-01T00:00:00Z&per_page=100&sort=updated&direction=asc" 2>/dev/null \
| jq -r ".[] | select(.closed_at < \"${CUTOFF}\") | .number" 2>/dev/null | while read -r num; do --jq ".[] | select(.closed_at < \"${CUTOFF}\") | .number" 2>/dev/null | while read -r num; do
# Lock and close with "not_planned" to mark as cleaned up # Lock and close with "not_planned" to mark as cleaned up
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${num}/lock" 2>/dev/null -X PUT -f lock_reason="resolved" --silent 2>/dev/null || true curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${num}/lock" 2>/dev/null -X PUT -f lock_reason="resolved" --silent 2>/dev/null || true
echo " Locked issue #${num}" >> $GITHUB_STEP_SUMMARY echo " Locked issue #${num}" >> $GITHUB_STEP_SUMMARY

View File

@@ -2577,7 +2577,7 @@ jobs:
gh label create "$LABEL" --repo "$REPO" --color "D73A4A" --description "Standards compliance failure" --force 2>/dev/null || true gh label create "$LABEL" --repo "$REPO" --color "D73A4A" --description "Standards compliance failure" --force 2>/dev/null || true
EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" 2>/dev/null \ EXISTING=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" 2>/dev/null \
2>/dev/null | jq -r '.[0].number') --jq '.[0].number' 2>/dev/null)
if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${EXISTING}" 2>/dev/null -X PATCH \ curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/issues/${EXISTING}" 2>/dev/null -X PATCH \
@@ -2585,7 +2585,7 @@ jobs:
echo "Updated issue #${EXISTING}" echo "Updated issue #${EXISTING}"
else else
gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" \ gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" \
--label "$LABEL" --assignee "jmiller" --label "$LABEL" --assignee "jmiller-moko"
fi fi
# CUSTOMIZATION: # CUSTOMIZATION:

View File

@@ -53,7 +53,7 @@ permissions:
jobs: jobs:
update-xml: update-xml:
name: Update updates.xml name: Update updates.xml
runs-on: release runs-on: ubuntu-latest
if: >- if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
@@ -294,9 +294,9 @@ jobs:
ACTOR="${{ github.actor }}" ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}" REPO="${{ github.repository }}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/collaborators/${ACTOR}/permission" 2>/dev/null \ PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/collaborators/${ACTOR}/permission" 2>/dev/null \
2>/dev/null | jq -r '.permission' || \ --jq '.permission' 2>/dev/null || \
curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/collaborators/${ACTOR}" 2>/dev/null \ curl -sf -H "Authorization: token ${{ secrets.GA_TOKEN }}" "${{GITEA_URL:-https://git.mokoconsulting.tech}}/api/v1/repos/${{ github.repository }}/collaborators/${ACTOR}" 2>/dev/null \
2>/dev/null | jq -r '.role' || echo "read") --jq '.role' 2>/dev/null || echo "read")
case "$PERMISSION" in case "$PERMISSION" in
admin|maintain|write) ;; admin|maintain|write) ;;
*) *)

9
.gitignore vendored
View File

@@ -198,10 +198,5 @@ venv/
*.coverage *.coverage
hypothesis/ hypothesis/
# Custom theme palettes (site-specific, not version controlled) src/media/css/theme/dark.custom.css
# Note: src/templates/*.custom.css are STARTER templates (tracked) src/media/css/theme/light.custom.css
src/media/css/theme/*.custom.css
src/media/css/theme/*.custom.min.css
templates/*.custom.css
update.xml
.moko-standards

20
.moko-standards Normal file
View File

@@ -0,0 +1,20 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION
# DEFGROUP: MokoStandards.Templates.Config
# INGROUP: MokoStandards.Templates
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
# PATH: /templates/configs/moko-standards.yml
# VERSION: 04.01.00
# BRIEF: Governance attachment template — synced to .moko-standards in every governed repository
# NOTE: Tokens replaced at sync time: mokoconsulting-tech, MokoCassiopeia, waas-component, 04.00.04
#
# This file is managed automatically by MokoStandards bulk sync.
# Do not edit manually — changes will be overwritten on the next sync.
# To update governance settings, open a PR in MokoStandards instead:
# https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
standards_source: "https://git.mokoconsulting.tech/MokoConsulting/MokoStandards"
standards_version: "04.00.04"
platform: "waas-component"
governed_repo: "MokoConsulting/MokoCassiopeia"

View File

@@ -19,48 +19,6 @@ All notable changes to the MokoCassiopeia Joomla template are documented in this
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [03.10.00] - 2026-04-18 — Bridge Release (MokoCassiopeia → MokoOnyx)
### Important
- **Template Rename** — MokoCassiopeia is being renamed to **MokoOnyx**. This bridge release automatically migrates your template settings, menu assignments, and files to the new name. MokoCassiopeia can be safely uninstalled after this update.
### Added
- **Offline page redesign** — Full-viewport background from Joomla offline_image or header background, glass card overlay, centered logo with glow, login accordion, copyright footer
- **CSS variable click-to-copy** — Text containing `--variable-name` patterns is wrapped in clickable chips that copy to clipboard with toast notification
- **Brand-aside 3-column layout** — Flex columns like top-a with card style
- **mod_stats table layout** — Converted from definition list to semantic table
- **Favicon multi-format support** — Now handles PNG, JPEG, GIF, WebP, BMP (not just PNG)
- **Theme variables** — `--theme-fab-bg`, `--theme-fab-color`, `--theme-fab-btn-bg`, `--theme-fab-border`, `--offline-card-bg`
- **Footer CSS variables** — Added to CSS Variables reference tab
- **Bridge migration script** — `helper/bridge.php` handles automatic MokoCassiopeia → MokoOnyx migration
- **Dedicated release runner** — Release workflows run on isolated `release` label runner
- **Runner fleet** — 3 CI + 1 release runner (12 concurrent jobs)
### Changed
- **Gitea-primary CI/CD** — All workflows use Gitea API, GitHub is backup for stable/RC only
- **Theme switcher** — Larger, bordered, theme-aware colors (off-white on dark, primary on light)
- **Auto switch** — Red when off, green when on
- **A11y toolbar** — Theme-aware colors for dark mode visibility
- **Search button border** — Matches input border (`--input-border-color`)
- **Offline message** — 0=hidden, 1=custom message, 2=system language string
- **Light theme fonts** — Fixed trailing `)` syntax error, normalized quote style to match dark
- **`--accent-color-secondary`** — Unified to `#6fb3ff` across both themes
- **`--alert-color`** — Set to `#000` in light theme
### Removed
- Brand showcase tab (redundant with theme preview)
- Position selectors for a11y/theme FAB (forced to bottom-right)
- Custom theme CSS from git tracking (site-specific, gitignored)
### Fixed
- SHA-256 checksum format — Removed `sha256:` prefix (Joomla expects raw hex)
- Favicon path resolution — Strips `#joomlaImage://` fragment, tries multiple path candidates
- `REQUIRE_SIGNIN_VIEW` — Set to `false` for public release downloads
- Release workflow — Uses Gitea API to update `updates.xml` on main (bypasses branch protection)
- Language loading on offline page — `com_users` and core language files loaded explicitly
---
## [Unreleased] - 2026-04-02 ## [Unreleased] - 2026-04-02
### Added ### Added

484
README.md
View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "mokoconsulting/mokocassiopeia", "name": "mokoconsulting/mokocassiopeia",
"description": "MokoCassiopeia \u00e2\u20ac\u201d Joomla site template based on Cassiopeia", "description": "MokoCassiopeia \u2014 Joomla site template based on Cassiopeia",
"type": "joomla-template", "type": "joomla-template",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"authors": [ "authors": [
@@ -10,8 +10,8 @@
} }
], ],
"require": { "require": {
"php": ">=8.1", "mokoconsulting-tech/enterprise": "dev-version/04",
"ext-zip": "*" "php": ">=8.1"
}, },
"require-dev": { "require-dev": {
"mokoconsulting-tech/enterprise": "^4.0" "mokoconsulting-tech/enterprise": "^4.0"

View File

@@ -1,31 +1,917 @@
<!-- <!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project. This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
# FILE INFORMATION This program is free software; you can redistribute it and/or modify
DEFGROUP: Joomla.Template.Site it under the terms of the GNU General Public License as published by
INGROUP: MokoCassiopeia.Documentation the Free Software Foundation; either version 3 of the License, or
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia (at your option) any later version.
PATH: /docs/ROADMAP.md
VERSION: 03.10.20 This program is distributed in the hope that it will be useful,
BRIEF: Redirect to MokoOnyx roadmap but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
# FILE INFORMATION
DEFGROUP: Joomla.Template.Site
INGROUP: MokoCassiopeia.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia
FILE: docs/ROADMAP.md
VERSION: 03.09.03
BRIEF: Version-specific roadmap for MokoCassiopeia template
PATH: /docs/ROADMAP.md
--> -->
# MokoCassiopeia Roadmap # MokoCassiopeia Roadmap (VERSION: 03.09.03)
**MokoCassiopeia has been renamed to MokoOnyx.** All future development continues under the MokoOnyx project. This document provides a comprehensive, version-specific roadmap for the MokoCassiopeia Joomla template, tracking feature evolution, current capabilities, and planned enhancements.
See the [MokoOnyx Roadmap](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/src/branch/dev/docs/ROADMAP.md) for all planned features and releases. ## Table of Contents
## Migration - [Version Timeline](#version-timeline)
- [Past Releases](#past-releases)
- [Future Roadmap (5-Year Plan)](#future-roadmap-5-year-plan)
- [Current Release (v03.06.03)](#current-release-v030603)
- [Implemented Features](#implemented-features)
- [Planned Features](#planned-features)
- [Development Priorities](#development-priorities)
- [Long-term Vision](#long-term-vision)
- [External Resources](#external-resources)
To migrate from MokoCassiopeia to MokoOnyx: ---
1. Download MokoOnyx from [Gitea Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/v01) ## Version Timeline
2. Install via **System → Install → Extensions**
3. Set MokoOnyx as default in **System → Site Templates** ### Past Releases
4. Visit any frontend page — settings are imported automatically
5. Uninstall MokoCassiopeia from **Extensions → Manage** ### v03.05.01 (2026-01-09) - Standards & Security
**Status**: Released (CHANGELOG entry exists, code files pending version update)
**Added**:
- Dependency review workflow for vulnerability scanning
- Standards compliance workflow for MokoStandards validation
- Dependabot configuration for automated security updates
- Documentation index (`docs/README.md`)
**Changed**:
- Removed custom CodeQL workflow (using GitHub's default setup)
- Enforced repository compliance with MokoStandards
- Improved security posture with automated scanning
### v03.06.00 (2026-01-28) - Version Update
**Status**: Current Release (in code)
**Changed**:
- Updated version to 03.06.00 across all files
### v03.05.00 (2026-01-04) - Workflow & Governance
**Status**: Mentioned in CHANGELOG (v03.05.00)
**Added**:
- `.github/workflows` directory structure
- CODE_OF_CONDUCT.md from MokoStandards
- CONTRIBUTING.md from MokoStandards
**Changed**:
- TODO items to be split to separate file (tracked)
### v03.01.00 (2025-12-16) - CI/CD Foundation
**Added**:
- Initial GitHub Actions workflows
### v03.00.00 (2025-12-09) - Font Awesome 7 Upgrade
**Updated**:
- Copyright headers to MokoCodingDefaults standards
- Fixed color style injection in `index.php`
- Upgraded Font Awesome 6 to Font Awesome 7 Free
- Added Font Awesome 7 Free style fallback
**Removed**:
- Deprecated CODE_OF_CONDUCT.md
- Deprecated CONTRIBUTING.md
### v02.01.05 (2025-09-04) - CSS Refinement
**Fixed**:
- Removed vmbasic.css
- Repaired template.css and colors_standard.css
### v02.00.00 (2025-08-30) - Dark Mode & TOC
**Major Features**:
- **Dark Mode Toggle System**
- Frontend toggle switch with localStorage persistence
- Admin-configurable default mode
- CSS rules for light/dark themes
- JavaScript-powered mode switching
- **Enhanced Template Parameters**
- Logo parameter support
- GTM container ID configuration
- Dark mode defaults in settings
- Updated metadata and copyright headers
- **Expanded Table of Contents**
- Automatic TOC injection
- User-selectable placement (`toc-left` or `toc-right`)
- Article options integration
**Improvements**:
- Cleaned up `index.php` (removed duplicate skip-to-content calls)
- Consolidated JavaScript asset loading
- Streamlined CSS for toggle switch
- Accessibility refinements (typography, color contrast)
- Fixed missing logo parameter in header
- Corrected stylesheet inconsistencies
- Patched redundant script includes
### v01.00.00 - Initial Public Release
**Core Features**:
- Font Awesome 6 integration
- Bootstrap 5 helpers and utilities
- Automatic Table of Contents (TOC) utility
- Moko Expansions: Google Tag Manager / GA4 hooks
- Built on Joomla's Cassiopeia template
---
### Future Roadmap (5-Year Plan)
The following versions represent our planned annual major releases, each building upon the previous version's foundation.
#### v04.00.00 (Q4 2027) - Enhanced Accessibility & Performance
**Status**: Planned
**Target Release**: December 2027
**Major Template Features**:
- **WCAG 2.1 AA Compliance**
- Full accessibility audit and remediation
- High-contrast theme options
- Screen reader optimizations
- Keyboard navigation enhancements
- ARIA landmark improvements
- Skip navigation enhancements
- **Template Performance Optimizations**
- Critical CSS inlining for faster first paint
- Lazy loading for images and below-fold content
- WebP image support with automatic fallbacks
- Advanced asset bundling and minification
- Template asset caching (CSS/JS bundles)
- **Enhanced Layout System**
- Additional responsive grid layouts
- Flexible module position system
- Column layout presets (2-col, 3-col, 4-col variations)
- Grid/masonry article layouts
- Sticky sidebar options
- **Typography Enhancements**
- Advanced typography controls in template settings
- Additional font pairing presets
- Custom font upload support
- Line height and letter spacing controls
- Responsive typography scaling
- **Developer Experience**
- Development mode enablement (unminified assets, debug output)
- Live reload during development
- Enhanced error logging and diagnostics
- Template debugging tools
- Style guide generator
- **Content Display Features**
- Soft offline mode (category-based access during maintenance)
- Enhanced article layouts (grid, masonry, timeline)
- Image caption styling options
- Quote block styling variations
- Enhanced breadcrumb customization
**Template Infrastructure**:
- Expanded template parameter validation
- Enhanced template override detection
- Automated template compatibility testing
- Template performance profiling tools
---
#### v05.00.00 (Q4 2028) - Advanced Layouts & Template Customization
**Status**: Planned
**Target Release**: December 2028
**Major Template Features**:
- **Enhanced Layout Builder**
- Template-based page layout variations
- Configurable layout options via template parameters
- Layout presets library (blog, portfolio, business, magazine)
- Module position layout manager
- Visual layout preview in admin
- **Advanced Styling System**
- Extended color palette management (unlimited custom palettes)
- CSS variable editor in template settings
- Style presets for different site types
- Border radius and spacing controls
- Box shadow and effect controls
- **Template Component Enhancements**
- Enhanced menu styling options (mega menu support)
- Advanced header variations (transparent, sticky, minimal)
- Footer layout options (column variations, widgets)
- Sidebar styling and behavior options
- Hero section templates and variations
- **Content Display Options**
- Article intro/full text display controls
- Category layout variations (grid, list, masonry, cards)
- Featured content sections
- Related articles display options
- Author bio box styling
- **Responsive Design Improvements**
- Mobile-first navigation patterns
- Tablet-specific layout controls
- Responsive image sizing options
- Mobile header variations
- Touch-friendly interface elements
- **Template Integration Features**
- Enhanced VirtueMart template overrides
- Contact form styling variations
- Search result layout options
- Error page customization
- Archive page templates
**Template Infrastructure**:
- Joomla 6.x template compatibility (if released)
- PHP 8.2+ support
- Template child theme support
- Template preset import/export functionality
---
#### v06.00.00 (Q4 2029) - Template Extensions & Advanced Features
**Status**: Planned
**Target Release**: December 2029
**Major Template Features**:
- **Template Marketplace & Extensions**
- Template addon system for modular features
- Community-contributed template extensions
- Template preset marketplace
- Style pack distribution system
- Template component library
- **Advanced Module System**
- Custom module chrome options
- Module animation effects
- Module visibility controls (scroll, time-based)
- Module group management
- Module style inheritance
- **Enhanced Media Handling**
- Background image options per page/section
- Image overlay controls
- Parallax scrolling effects
- Video background support
- Gallery template variations
- **Template Branding Options**
- Multiple logo upload (standard, retina, mobile)
- Favicon and app icon management
- Custom loading screen/animations
- Watermark options
- Brand color scheme generator
- **Advanced Header/Footer**
- Multiple header layout presets
- Sticky header variations and behaviors
- Header transparency controls
- Footer widget areas expansion
- Floating action buttons
- **Content Enhancement Features**
- Reading progress indicator
- Social sharing buttons (template-integrated)
- Print-friendly styles
- Reading time estimation display
- Content table enhancements
- **Template SEO Features**
- Schema markup templates for common types
- Open Graph tag management
- Twitter Card support
- Breadcrumb schema integration
- Meta tag template controls
**Template Infrastructure**:
- Template versioning system
- Template backup/restore functionality
- Template A/B testing support
- Multi-language template variations
- Template documentation generator
---
#### v07.00.00 (Q4 2030) - Modern Template Standards & Enhancements
**Status**: Planned
**Target Release**: December 2030
**Major Template Features**:
- **Modern CSS Features**
- CSS Grid layout system integration
- CSS Container Queries support
- CSS Cascade Layers implementation (layered style priority system)
- Custom properties (CSS variables) UI
- Modern filter and backdrop effects
- **Progressive Template Features**
- Offline-capable template assets
- Service worker template integration
- App manifest generation
- Install to home screen support
- Template asset preloading strategies
- **Animation & Interaction**
- Scroll-triggered animations
- Hover effect library
- Page transition effects
- Micro-interactions for UI elements
- Loading animation options
- **Advanced Responsive Features**
- Container-based responsive design
- Element visibility by viewport
- Responsive navigation patterns library
- Mobile-optimized interactions
- Adaptive image loading
- **Template Accessibility Features**
- Focus indicators customization
- Reduced motion preferences support
- High contrast mode automation
- Keyboard navigation patterns
- ARIA live regions for dynamic content
- **Content Presentation**
- Advanced blockquote styles
- Code snippet highlighting themes
- Table styling variations
- List styling options
- Custom content block templates
- **Template Performance**
- Resource hints (preconnect, prefetch)
- Optimal asset delivery strategies
- Image format optimization (AVIF support)
- Font loading optimization
- Template metrics dashboard
**Template Infrastructure**:
- Template pattern library
- Design token system
- Template component documentation
- Automated template testing suite
- Template performance monitoring
---
#### v08.00.00 (Q4 2031) - Next-Generation Template Features
**Status**: Conceptual
**Target Release**: December 2031
**Major Template Features**:
- **Advanced Layout Systems**
- Subgrid support for complex layouts
- Multi-column layout variations
- Asymmetric grid systems
- Dynamic layout switching
- Layout constraint system
- **Enhanced Visual Customization**
- Real-time style editor
- Template style variations manager
- Custom CSS injection with validation
- Style inheritance and override system
- Visual design tokens editor
- **Template Component Library**
- Comprehensive UI component set
- Reusable template blocks
- Component variation system
- Template snippet library
- Pattern library integration
- **Advanced Typography System**
- Variable font support
- Advanced typographic scales
- Font pairing recommendations
- Fluid typography system
- Custom font fallback chains
- **Template Integration Features**
- Enhanced component overrides
- Template hooks system
- Event-based template modifications
- Custom field rendering templates
- Module position API enhancements
- **Responsive & Adaptive Design**
- Advanced breakpoint management
- Element-specific responsive controls
- Adaptive images with art direction
- Responsive typography system
- Context-aware component rendering
- **Template Ecosystem**
- Child template framework
- Template derivative system
- Community template marketplace
- Template rating and review system
- Professional template support network
- **Template Quality & Maintenance**
- Automated accessibility testing
- Template performance auditing
- Code quality monitoring
- Update notification system
- Template health dashboard
**Template Infrastructure**:
- Template API for extensibility
- Template package manager
- Template development CLI tools
- Template migration utilities
- Comprehensive template documentation system
---
## Current Release (v03.06.03)
### System Requirements
- **Joomla**: 4.4.x or 5.x
- **PHP**: 8.0+
- **Database**: MySQL/MariaDB compatible
### Architecture
- **Base Template**: Joomla Cassiopeia
- **Enhancement Layer**: Non-invasive overrides
- **Asset Management**: Joomla Web Asset Manager (WAM)
- **Frontend Framework**: Bootstrap 5
- **Icon Library**: Font Awesome 7 Free
---
## Implemented Features
### 🎨 Theming & Visual Design
#### Color Palette System
- **3 Built-in Palettes**: Standard, Alternative, Custom
- **Dual Mode Support**: Separate light and dark configurations
- **Custom Palettes**: User-definable via `colors_custom.css`
- **Location**: `src/media/css/colors/{light|dark}/`
#### Dark Mode System
- **Toggle Controls**: Switch (Light↔Dark) or Radios (Light/Dark/System)
- **Default Mode**: Admin-configurable (system, light, or dark)
- **Persistence**: localStorage for user preferences
- **Auto-Detection**: Optional system preference detection
- **Meta Tags**: `color-scheme` and `theme-color` support
- **ARIA Bridge**: Bootstrap ARIA compatibility
#### Typography
- **Font Schemes**:
- Local: Roboto
- Web (Google Fonts): Fira Sans, Roboto + Noto Sans
- **Admin-Configurable**: Template settings dropdown
#### Branding
- **Logo Support**: Custom logo upload
- **Site Title**: Text-based branding option
- **Site Description**: Tagline/subtitle field
- **Font Awesome Kit**: Optional custom kit integration
### 📐 Layout & Structure
#### Module Positions (23 Total)
**Header Area**:
- topbar, below-topbar, below-logo, menu, search, banner
**Content Area**:
- top-a, top-b, main-top, main-bottom, breadcrumbs
- sidebar-left, sidebar-right
**Footer Area**:
- bottom-a, bottom-b, footer-menu, footer
**Special**:
- debug, offline-header, offline, offline-footer
- drawer-left, drawer-right
#### Layout Options
- **Container Type**: Fluid or Static
- **Sticky Header**: Optional fixed navigation
- **Back-to-Top Button**: Scrollable page support
### 📝 Content Features
#### Table of Contents (TOC)
- **Automatic Generation**: From article headings
- **Placement Options**: `toc-left` or `toc-right` layouts
- **Article Integration**: Via Options → Layout dropdown
- **Responsive**: Mobile-friendly sidebar placement
#### Article Layouts
- **Default**: Standard Cassiopeia layout
- **TOC Variants**: Left-sidebar or right-sidebar TOC
- **Custom Overrides**: Located in `html/com_content/article/`
### 📊 Analytics & Tracking
#### Google Tag Manager (GTM)
- **Enable/Disable**: Admin toggle
- **Container ID**: Template parameter field
- **Implementation**: Head and body script injection
- **GDPR-Ready**: Configurable consent defaults
#### Google Analytics 4 (GA4)
- **Enable/Disable**: Admin toggle
- **Property ID**: Template parameter field
- **Universal Analytics Fallback**: Legacy UA support
- **Privacy-First**: Conditional loading based on settings
### 🎛️ Customization & Developer Tools
#### Custom Code Injection
- **Head Start**: Custom HTML/JS before `</head>`
- **Head End**: Custom HTML/JS at end of `<head>`
- **Raw HTML**: Unfiltered code injection for advanced users
#### Drawer System
- **Left/Right Drawers**: Offcanvas menu areas
- **Icon Customization**: Font Awesome icon selection
- **Default Icons**:
- Left: `fa-solid fa-chevron-right`
- Right: `fa-solid fa-chevron-left`
#### Asset Management
- **Joomla WAM**: Complete asset registry in `joomla.asset.json`
- **Development/Production Modes**: Minified and unminified assets
- **Dependency Management**: Automatic script/style loading
### 🏗️ Template Overrides
#### Component Overrides
**Content (com_content)**:
- Article layouts (default, toc-left, toc-right)
- Category layouts (blog, list)
- Featured articles
**Contact (com_contact)**:
- Contact form layouts
**Engage (com_engage)**:
- Comment system integration
#### Module Overrides
**Menu (mod_menu)**:
- Metis dropdown menu
- Offcanvas navigation
**VirtueMart**:
- Product display (`mod_virtuemart_product`)
- Shopping cart (`mod_virtuemart_cart`)
- Manufacturer display (`mod_virtuemart_manufacturer`)
- Category display (`mod_virtuemart_category`)
- Currency selector (`mod_virtuemart_currencies`)
**Other Modules**:
- Custom HTML (`mod_custom`)
- GABble social integration (`mod_gabble`)
**Membership System (OS Membership)**:
- Plan layouts (default, pricing tables)
- Member management interfaces
### 🔧 Configuration Parameters
#### Theme Tab
**General**:
- `theme_enabled` - Enable/disable theme system
- `theme_control_type` - Toggle UI type (switch/radios/none)
- `theme_default_choice` - Default mode (system/light/dark)
- `theme_auto_dark` - Auto-detect system preference
- `theme_meta_color_scheme` - Inject `color-scheme` meta tag
- `theme_meta_theme_color` - Inject `theme-color` meta tag
- `theme_bridge_bs_aria` - Bootstrap ARIA compatibility
**Variables & Palettes**:
- `colorLightName` - Light mode color scheme
- `colorDarkName` - Dark mode color scheme
**Typography**:
- `useFontScheme` - Font selection (local/web)
**Branding & Icons**:
- `brand` - Show/hide branding
- `logoFile` - Logo upload path
- `siteTitle` - Site title text
- `siteDescription` - Site tagline
- `fA6KitCode` - Font Awesome kit code
**Header & Navigation**:
- `stickyHeader` - Fixed navigation
- `backTop` - Back-to-top button
**Toggle UI**:
- `theme_fab_enabled` - Floating action button for theme toggle
- `theme_fab_pos` - FAB position (br/bl/tr/tl)
#### Google Tab
- `googletagmanager` - Enable GTM
- `googletagmanagerid` - GTM container ID
- `googleanalytics` - Enable GA4
- `googleanalyticsid` - GA4 property ID
#### Custom Code Tab
- `custom_head_start` - Custom code at head start
- `custom_head_end` - Custom code at head end
#### Drawers Tab
- `drawerLeftIcon` - Left drawer icon (Font Awesome class)
- `drawerRightIcon` - Right drawer icon (Font Awesome class)
#### Advanced Tab
- `fluidContainer` - Container layout (static/fluid)
### 🛠️ Development Tools
#### Quality Assurance
- **Codeception**: Automated testing framework
- **PHPStan**: Static analysis (level 8+)
- **PHPCS**: Code style validation (PSR-12)
- **PHPCompatibility**: PHP 8.0+ compatibility checks
#### CI/CD Workflows
- **Dependency Review**: Vulnerability scanning
- **Standards Compliance**: MokoStandards validation
- **CodeQL**: Security analysis (GitHub default)
- **Dependabot**: Automated dependency updates
#### Documentation
- **Quick Start**: 5-minute developer setup
- **Workflow Guide**: Git strategy, branching, releases
- **Joomla Development**: Testing, packaging, multi-version support
---
## Planned Features
### 🚧 In Development
#### Soft Offline Mode (v03.07.00 - Planned)
**Status**: Planned for v03.07.00
**Priority**: High
**Description**: Keep selected categories accessible during site maintenance mode with persistent links to essential pages
**Use Cases**:
- Legal documents remain viewable during downtime
- Policy pages accessible for compliance requirements
- Terms of service always available to users
- Privacy policy accessible at all times
- Essential public information during maintenance
**Technical Specifications**:
- **Configuration Method**: Template parameters in `templateDetails.xml`
- **Category Access**: Category IDs stored as comma-separated values
- **Persistent Links**: Direct article/menu item links always visible
- **Access Control**: Check in `offline.php` template file
- **Content Rendering**: Use Joomla's content component to fetch articles
- **Security**: Maintain proper access levels and permissions
**Implementation Plan**:
1. Add category selection field to template parameters
2. Add persistent link configuration (Terms of Service, Privacy Policy, etc.)
3. Modify `offline.php` to check for allowed categories
4. Add persistent link display in offline mode header/footer
5. Implement category content fetching during offline mode
6. Add styling for offline mode category display and persistent links
7. Test with various category and link configurations
8. Document admin configuration steps
**Configuration Interface**:
- **Category Field Type**: Category multiselect in template settings
- **Label**: "Categories Accessible During Offline Mode"
- **Default**: None (all content hidden by default)
- **Persistent Links**: Text fields for essential always-available links
- **Terms of Service URL**: Direct link to TOS article/page
- **Privacy Policy URL**: Direct link to privacy policy
- **Contact URL**: Optional contact page link
- **Custom Link 1-3**: Additional persistent links if needed
- **Admin Path**: System → Site Templates → MokoCassiopeia → Advanced → Offline Mode Settings
**Persistent Links Feature**:
- **Display Location**: Footer of offline page
- **Styling**: Clearly visible, accessible links
- **Format**: "Terms of Service | Privacy Policy | Contact"
- **Behavior**: Links bypass offline mode restrictions
- **Validation**: Check if URLs are valid Joomla routes
**Benefits**:
- ✅ Compliance: Keep legal pages accessible
- ✅ Transparency: Users can access essential information
- ✅ Flexibility: Admin control over which categories remain visible
- ✅ Security: Respects Joomla access levels
- ✅ Legal Protection: Terms of Service always accessible
- ✅ User Trust: Privacy policy always available
**Milestone**: Target release v03.07.00 (Q2 2026)
#### TODO Tracking System
**Status**: Mentioned in CHANGELOG (v03.05.00)
**Description**: Separate TODO tracking file
**Purpose**: Centralized issue and feature tracking outside changelog
### 🔮 Future Enhancements
#### Development Mode (Commented Out)
**Status**: Code exists but disabled
**Location**: `templateDetails.xml` line 91
**Description**: Comprehensive development mode toggle
**Potential Features**:
- Unminified asset loading
- Debug output
- Performance profiling
- Template cache bypass
#### Potential Features (Community Requested)
*Note: These are conceptual and not yet officially planned*
**Enhanced Accessibility**:
- WCAG 2.1 AAA compliance mode
- High-contrast themes
- Screen reader optimizations
- Keyboard navigation improvements
**Template Layout Features**:
- Advanced responsive grid layouts
- Multiple column variations
- Custom module position system
- Layout preset library
**Template Styling Features**:
- Extended color palette management
- Custom font upload support
- Typography scale controls
- Visual style editor
---
## Development Priorities
### Immediate Focus (v03.x - 2026)
1. **Bootstrap TOC Integration**: Complete and document v1.0.1 implementation ✅
2. **Soft Offline Mode**: Implement category-based offline access (Target: v03.07.00)
3. **TODO Tracking System**: Implement separate file for issue tracking
4. **Security Updates**: Maintain Dependabot and CodeQL scans
5. **Documentation**: Keep docs synchronized with features
6. **Bug Fixes**: Address reported issues and edge cases
### v04.00.00 Priorities (2027) - Template Foundation
1. **WCAG 2.1 AA Compliance**: Full template accessibility audit and implementation
2. **Template Performance**: Critical CSS, lazy loading, WebP support
3. **Layout System**: Enhanced responsive grid and module positions
4. **Development Mode**: Enable comprehensive template developer tools
### v05.00.00 Priorities (2028) - Template Customization
1. **Layout Builder**: Template-based page layout system
2. **Styling System**: Extended color palettes and CSS variable management
3. **Template Components**: Enhanced header, footer, and menu variations
4. **Responsive Design**: Mobile-first navigation and layout improvements
### v06.00.00 Priorities (2029) - Template Extensions
1. **Template Marketplace**: Addon system and community extensions
2. **Module System**: Advanced module chrome and animation options
3. **Media Handling**: Background images, parallax, video backgrounds
4. **Template SEO**: Schema markup templates and meta tag controls
### v07.00.00+ Priorities (2030+) - Modern Standards
1. **Modern CSS**: Grid, Container Queries, Cascade Layers
2. **Progressive Template**: Offline-capable assets and PWA features
3. **Animation System**: Scroll-triggered effects and micro-interactions
4. **Template Performance**: Advanced optimization and monitoring
---
## Long-term Vision
### Mission Statement
MokoCassiopeia aims to be the **most developer-friendly, user-customizable, and standards-compliant Joomla template** while maintaining minimal core overrides for maximum upgrade compatibility.
### Core Principles
1. **Non-Invasive**: Minimal Cassiopeia overrides
2. **Standards-First**: MokoStandards compliance
3. **Accessibility**: WCAG 2.1 compliance
4. **Performance**: Fast, optimized delivery
5. **Developer Experience**: Clear docs, easy setup, powerful tools
6. **Template-Focused**: Pure template features without complex external dependencies
### 5-Year Strategic Roadmap (Template Features)
#### 2027 (v04.00.00) - Accessibility & Performance
- Achieve WCAG 2.1 AA compliance for all template elements
- Implement critical template performance optimizations
- Enhance template layout system with flexible grids
- Enable comprehensive development mode for template developers
#### 2028 (v05.00.00) - Layouts & Customization
- Launch template-based layout builder system
- Deploy extended styling and customization options
- Enhance template component variations (headers, footers, menus)
- Improve responsive design patterns for all devices
#### 2029 (v06.00.00) - Extensions & Enhancements
- Introduce template addon and extension system
- Launch template preset marketplace
- Deploy advanced module styling and animation features
- Implement comprehensive template SEO controls
#### 2030 (v07.00.00) - Modern Standards
- Adopt modern CSS standards (Grid, Container Queries, Cascade Layers)
- Implement progressive template features (PWA support)
- Deploy advanced animation and interaction system
- Enhance template performance monitoring and optimization
#### 2031 (v08.00.00) - Next-Generation Template
- Advanced layout systems with subgrid support
- Comprehensive template component library
- Enhanced visual customization tools
- Template ecosystem with child themes and derivatives
---
## External Resources
### Official Links
- **Full Roadmap**: [https://mokoconsulting.tech/support/joomla-cms/mokocassiopeia-roadmap](https://mokoconsulting.tech/support/joomla-cms/mokocassiopeia-roadmap)
- **Repository**: [https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia](https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia)
- **Issue Tracker**: [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia/issues)
- **Changelog**: [CHANGELOG.md](../CHANGELOG.md)
### Community
- **Email Support**: hello@mokoconsulting.tech
- **Contributing**: [CONTRIBUTING.md](../CONTRIBUTING.md)
- **Code of Conduct**: [CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)
### Documentation
- **Quick Start**: [docs/QUICK_START.md](./QUICK_START.md)
- **Workflow Guide**: [docs/WORKFLOW_GUIDE.md](./WORKFLOW_GUIDE.md)
- **Joomla Development**: [docs/JOOMLA_DEVELOPMENT.md](./JOOMLA_DEVELOPMENT.md)
- **Main README**: [README.md](../README.md)
---
## Contributing to the Roadmap
Have ideas for future features? We welcome community input!
**How to Suggest Features**:
1. Check the [GitHub Issues](https://git.mokoconsulting.tech/MokoConsulting/moko-cassiopeia/issues) for existing requests
2. Open a new issue with the `enhancement` label
3. Provide clear use cases and benefits
4. Engage in community discussion
**Feature Evaluation Criteria**:
- Alignment with core principles
- User demand and use cases
- Technical feasibility
- Maintenance burden
- Performance impact
- Security implications
---
## Metadata
* Document: docs/ROADMAP.md
* Repository: [https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia](https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia)
* Path: /docs/ROADMAP.md
* Owner: Moko Consulting
* Version: 03.06.03
* Status: Active
* Effective Date: 2026-01-30
* Classification: Public Open Source Documentation
## Revision History
| Date | Change Summary | Author |
| ---------- | ----------------------------------------------------- | --------------- |
| 2026-01-27 | Initial version-specific roadmap generated from codebase scan. | GitHub Copilot |
| 2026-01-27 | Added 5-year future roadmap with annual major version releases (v04-v08). | GitHub Copilot |
| 2026-01-27 | Refocused roadmap to concentrate on template-oriented features only. | GitHub Copilot |

View File

@@ -1,6 +1,6 @@
<?php <?php
/** /**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* *
* This file is part of a Moko Consulting project. * This file is part of a Moko Consulting project.
* *
@@ -9,35 +9,37 @@
/** /**
* Favicon generator — creates ICO, Apple Touch Icon, and Android icons * Favicon generator — creates ICO, Apple Touch Icon, and Android icons
* from a single source image uploaded via the template config. * from a single source PNG uploaded via the template config.
*
* Supports three backends in priority order:
* 1. GD (fastest, most common)
* 2. Imagick (common on shared hosting)
* 3. Pure PHP (zero-dependency fallback using raw PNG manipulation)
*/ */
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Log\Log;
class MokoFaviconHelper class MokoFaviconHelper
{ {
/**
* Sizes to generate: filename => [width, height, format].
* ICO embeds 16×16 and 32×32 internally.
*/
private const SIZES = [ private const SIZES = [
'apple-touch-icon.png' => [180, 180], 'apple-touch-icon.png' => [180, 180, 'png'],
'favicon-32x32.png' => [32, 32], 'favicon-32x32.png' => [32, 32, 'png'],
'favicon-16x16.png' => [16, 16], 'favicon-16x16.png' => [16, 16, 'png'],
'android-chrome-192x192.png' => [192, 192], 'android-chrome-192x192.png' => [192, 192, 'png'],
'android-chrome-512x512.png' => [512, 512], 'android-chrome-512x512.png' => [512, 512, 'png'],
]; ];
/** /**
* Generate all favicon files from a source image. * Generate all favicon files from a source PNG if they don't already exist
* or if the source has been modified since last generation.
*
* @param string $sourcePath Absolute path to the source PNG.
* @param string $outputDir Absolute path to the output directory.
*
* @return bool True if generation succeeded or files are up to date.
*/ */
public static function generate(string $sourcePath, string $outputDir): bool public static function generate(string $sourcePath, string $outputDir): bool
{ {
if (!is_file($sourcePath)) { if (!is_file($sourcePath) || !extension_loaded('gd')) {
self::log('Favicon: source file not found: ' . $sourcePath, 'warning');
return false; return false;
} }
@@ -48,424 +50,84 @@ class MokoFaviconHelper
$sourceTime = filemtime($sourcePath); $sourceTime = filemtime($sourcePath);
$stampFile = $outputDir . '/.favicon_generated'; $stampFile = $outputDir . '/.favicon_generated';
// Skip if already up to date
if (is_file($stampFile) && filemtime($stampFile) >= $sourceTime) { if (is_file($stampFile) && filemtime($stampFile) >= $sourceTime) {
return true; return true;
} }
// Strip #joomlaImage fragment if present $source = imagecreatefrompng($sourcePath);
$sourcePath = strtok($sourcePath, '#');
// Select backend
if (extension_loaded('gd')) {
$result = self::generateWithGd($sourcePath, $outputDir);
} elseif (extension_loaded('imagick')) {
$result = self::generateWithImagick($sourcePath, $outputDir);
} else {
$result = self::generatePurePHP($sourcePath, $outputDir);
}
if ($result) {
self::generateManifest($outputDir);
file_put_contents($stampFile, date('c'));
}
return $result;
}
// ── GD Backend ──────────────────────────────────────────────────
private static function generateWithGd(string $sourcePath, string $outputDir): bool
{
$imageInfo = @getimagesize($sourcePath);
if ($imageInfo === false) {
self::log('Favicon: cannot read image: ' . $sourcePath, 'warning');
return false;
}
$source = match ($imageInfo[2]) {
IMAGETYPE_PNG => @imagecreatefrompng($sourcePath),
IMAGETYPE_JPEG => @imagecreatefromjpeg($sourcePath),
IMAGETYPE_GIF => @imagecreatefromgif($sourcePath),
IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($sourcePath) : false,
default => false,
};
if (!$source) { if (!$source) {
self::log('Favicon: unsupported image type', 'warning');
return false; return false;
} }
imagealphablending($source, false); imagealphablending($source, false);
imagesavealpha($source, true); imagesavealpha($source, true);
$srcW = imagesx($source); $srcW = imagesx($source);
$srcH = imagesy($source); $srcH = imagesy($source);
// Generate PNG sizes
foreach (self::SIZES as $filename => [$w, $h]) { foreach (self::SIZES as $filename => [$w, $h]) {
$resized = imagecreatetruecolor($w, $h); $resized = imagecreatetruecolor($w, $h);
imagealphablending($resized, false); imagealphablending($resized, false);
imagesavealpha($resized, true); imagesavealpha($resized, true);
imagefill($resized, 0, 0, imagecolorallocatealpha($resized, 0, 0, 0, 127)); $transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127);
imagefill($resized, 0, 0, $transparent);
imagecopyresampled($resized, $source, 0, 0, 0, 0, $w, $h, $srcW, $srcH); imagecopyresampled($resized, $source, 0, 0, 0, 0, $w, $h, $srcW, $srcH);
imagepng($resized, $outputDir . '/' . $filename, 9); imagepng($resized, $outputDir . '/' . $filename, 9);
imagedestroy($resized); imagedestroy($resized);
} }
// ICO from GD // Generate ICO (contains 16×16 and 32×32)
$icoEntries = []; self::generateIco($source, $srcW, $srcH, $outputDir . '/favicon.ico');
// Generate site.webmanifest
self::generateManifest($outputDir);
imagedestroy($source);
// Write timestamp stamp
file_put_contents($stampFile, date('c'));
return true;
}
/**
* Build a minimal ICO file containing 16×16 and 32×32 PNG entries.
*/
private static function generateIco(\GdImage $source, int $srcW, int $srcH, string $outPath): void
{
$entries = [];
foreach ([16, 32] as $size) { foreach ([16, 32] as $size) {
$resized = imagecreatetruecolor($size, $size); $resized = imagecreatetruecolor($size, $size);
imagealphablending($resized, false); imagealphablending($resized, false);
imagesavealpha($resized, true); imagesavealpha($resized, true);
imagefill($resized, 0, 0, imagecolorallocatealpha($resized, 0, 0, 0, 127)); $transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127);
imagefill($resized, 0, 0, $transparent);
imagecopyresampled($resized, $source, 0, 0, 0, 0, $size, $size, $srcW, $srcH); imagecopyresampled($resized, $source, 0, 0, 0, 0, $size, $size, $srcW, $srcH);
ob_start(); ob_start();
imagepng($resized, null, 9); imagepng($resized, null, 9);
$icoEntries[] = ['size' => $size, 'data' => ob_get_clean()]; $pngData = ob_get_clean();
imagedestroy($resized); imagedestroy($resized);
}
self::writeIco($icoEntries, $outputDir . '/favicon.ico');
imagedestroy($source); $entries[] = ['size' => $size, 'data' => $pngData];
self::log('Favicon: generated with GD');
return true;
} }
// ── Imagick Backend ───────────────────────────────────────────── // ICO header: 2 bytes reserved, 2 bytes type (1=ICO), 2 bytes count
private static function generateWithImagick(string $sourcePath, string $outputDir): bool
{
try {
foreach (self::SIZES as $filename => [$w, $h]) {
$img = new \Imagick($sourcePath);
$img->setImageFormat('png');
$img->setImageCompressionQuality(95);
$img->thumbnailImage($w, $h, true);
// Center on transparent canvas if not square
$canvas = new \Imagick();
$canvas->newImage($w, $h, new \ImagickPixel('transparent'), 'png');
$offsetX = (int)(($w - $img->getImageWidth()) / 2);
$offsetY = (int)(($h - $img->getImageHeight()) / 2);
$canvas->compositeImage($img, \Imagick::COMPOSITE_OVER, $offsetX, $offsetY);
$canvas->writeImage($outputDir . '/' . $filename);
$img->destroy();
$canvas->destroy();
}
// ICO from Imagick
$icoEntries = [];
foreach ([16, 32] as $size) {
$img = new \Imagick($sourcePath);
$img->setImageFormat('png');
$img->thumbnailImage($size, $size, true);
$icoEntries[] = ['size' => $size, 'data' => (string) $img];
$img->destroy();
}
self::writeIco($icoEntries, $outputDir . '/favicon.ico');
self::log('Favicon: generated with Imagick');
return true;
} catch (\Exception $e) {
self::log('Favicon: Imagick failed: ' . $e->getMessage(), 'warning');
return false;
}
}
// ── Pure PHP Backend (zero dependencies) ────────────────────────
private static function generatePurePHP(string $sourcePath, string $outputDir): bool
{
$pngData = @file_get_contents($sourcePath);
if ($pngData === false) {
self::log('Favicon: cannot read source file', 'warning');
return false;
}
// Detect format — we can only resize PNG in pure PHP
// For JPEG/other formats, just copy the source as-is for each size
$isPng = (substr($pngData, 0, 8) === "\x89PNG\r\n\x1a\n");
if (!$isPng) {
// Non-PNG: copy source file for all sizes (no resize capability without extensions)
foreach (self::SIZES as $filename => [$w, $h]) {
copy($sourcePath, $outputDir . '/' . $filename);
}
// ICO: embed the raw source for 16 and 32 entries
self::writeIco([
['size' => 16, 'data' => $pngData],
['size' => 32, 'data' => $pngData],
], $outputDir . '/favicon.ico');
self::log('Favicon: non-PNG source copied without resize (no GD/Imagick)');
return true;
}
// Parse PNG dimensions from IHDR
$ihdr = self::parsePngIhdr($pngData);
if (!$ihdr) {
self::log('Favicon: cannot parse PNG header', 'warning');
return false;
}
$srcW = $ihdr['width'];
$srcH = $ihdr['height'];
// Decode PNG to raw RGBA pixel array
$pixels = self::decodePngToRgba($pngData, $srcW, $srcH, $ihdr);
if ($pixels === null) {
// Fallback: copy source for all sizes
foreach (self::SIZES as $filename => [$w, $h]) {
copy($sourcePath, $outputDir . '/' . $filename);
}
self::writeIco([
['size' => 16, 'data' => $pngData],
['size' => 32, 'data' => $pngData],
], $outputDir . '/favicon.ico');
self::log('Favicon: PNG decode failed, copied source without resize');
return true;
}
// Generate resized PNGs
foreach (self::SIZES as $filename => [$w, $h]) {
$resized = self::resizePixels($pixels, $srcW, $srcH, $w, $h);
$png = self::encodePng($resized, $w, $h);
file_put_contents($outputDir . '/' . $filename, $png);
}
// ICO
$icoEntries = [];
foreach ([16, 32] as $size) {
$resized = self::resizePixels($pixels, $srcW, $srcH, $size, $size);
$icoEntries[] = ['size' => $size, 'data' => self::encodePng($resized, $size, $size)];
}
self::writeIco($icoEntries, $outputDir . '/favicon.ico');
self::log('Favicon: generated with pure PHP');
return true;
}
/**
* Parse PNG IHDR chunk.
*/
private static function parsePngIhdr(string $data): ?array
{
if (strlen($data) < 33) return null;
// Skip 8-byte signature, 4-byte chunk length, 4-byte "IHDR"
$width = unpack('N', substr($data, 16, 4))[1];
$height = unpack('N', substr($data, 20, 4))[1];
$bitDepth = ord($data[24]);
$colorType = ord($data[25]);
return ['width' => $width, 'height' => $height, 'bitDepth' => $bitDepth, 'colorType' => $colorType];
}
/**
* Decode PNG to flat RGBA array using zlib decompression.
*
* @return array|null Flat array of [r,g,b,a, r,g,b,a, ...] or null on failure.
*/
private static function decodePngToRgba(string $data, int $w, int $h, array $ihdr): ?array
{
// Only support 8-bit RGBA (color type 6) and RGB (color type 2) for simplicity
$colorType = $ihdr['colorType'];
$bitDepth = $ihdr['bitDepth'];
if ($bitDepth !== 8 || ($colorType !== 6 && $colorType !== 2 && $colorType !== 3)) {
return null; // Unsupported format
}
// Collect all IDAT chunks
$idatData = '';
$pos = 8; // Skip PNG signature
$palette = null;
$trns = null;
while ($pos < strlen($data) - 4) {
$chunkLen = unpack('N', substr($data, $pos, 4))[1];
$chunkType = substr($data, $pos + 4, 4);
if ($chunkType === 'IDAT') {
$idatData .= substr($data, $pos + 8, $chunkLen);
} elseif ($chunkType === 'PLTE') {
$palette = substr($data, $pos + 8, $chunkLen);
} elseif ($chunkType === 'tRNS') {
$trns = substr($data, $pos + 8, $chunkLen);
} elseif ($chunkType === 'IEND') {
break;
}
$pos += 12 + $chunkLen; // 4 len + 4 type + data + 4 crc
}
$raw = @gzuncompress($idatData);
if ($raw === false) {
$raw = @gzinflate($idatData);
}
if ($raw === false) {
// Try with zlib header
$raw = @gzinflate(substr($idatData, 2));
}
if ($raw === false) {
return null;
}
$bpp = $colorType === 6 ? 4 : ($colorType === 2 ? 3 : 1); // bytes per pixel
$stride = 1 + $w * $bpp; // +1 for filter byte per row
$pixels = [];
$prevRow = array_fill(0, $w * $bpp, 0);
for ($y = 0; $y < $h; $y++) {
$rowStart = $y * $stride;
if ($rowStart >= strlen($raw)) break;
$filter = ord($raw[$rowStart]);
$row = [];
for ($x = 0; $x < $w * $bpp; $x++) {
$rawByte = ord($raw[$rowStart + 1 + $x]);
$a = ($x >= $bpp) ? $row[$x - $bpp] : 0;
$b = $prevRow[$x];
$c = ($x >= $bpp) ? $prevRow[$x - $bpp] : 0;
$val = match ($filter) {
0 => $rawByte,
1 => ($rawByte + $a) & 0xFF,
2 => ($rawByte + $b) & 0xFF,
3 => ($rawByte + (int)(($a + $b) / 2)) & 0xFF,
4 => ($rawByte + self::paethPredictor($a, $b, $c)) & 0xFF,
default => $rawByte,
};
$row[] = $val;
}
// Convert row to RGBA
for ($x = 0; $x < $w; $x++) {
if ($colorType === 6) { // RGBA
$pixels[] = $row[$x * 4];
$pixels[] = $row[$x * 4 + 1];
$pixels[] = $row[$x * 4 + 2];
$pixels[] = $row[$x * 4 + 3];
} elseif ($colorType === 2) { // RGB
$pixels[] = $row[$x * 3];
$pixels[] = $row[$x * 3 + 1];
$pixels[] = $row[$x * 3 + 2];
$pixels[] = 255;
} elseif ($colorType === 3 && $palette) { // Indexed
$idx = $row[$x];
$pixels[] = ord($palette[$idx * 3]);
$pixels[] = ord($palette[$idx * 3 + 1]);
$pixels[] = ord($palette[$idx * 3 + 2]);
$pixels[] = ($trns && $idx < strlen($trns)) ? ord($trns[$idx]) : 255;
}
}
$prevRow = $row;
}
return $pixels;
}
private static function paethPredictor(int $a, int $b, int $c): int
{
$p = $a + $b - $c;
$pa = abs($p - $a);
$pb = abs($p - $b);
$pc = abs($p - $c);
if ($pa <= $pb && $pa <= $pc) return $a;
if ($pb <= $pc) return $b;
return $c;
}
/**
* Bilinear resize of RGBA pixel array.
*/
private static function resizePixels(array $src, int $srcW, int $srcH, int $dstW, int $dstH): array
{
$dst = [];
$xRatio = $srcW / $dstW;
$yRatio = $srcH / $dstH;
for ($y = 0; $y < $dstH; $y++) {
$srcY = $y * $yRatio;
$y0 = (int) $srcY;
$y1 = min($y0 + 1, $srcH - 1);
$yFrac = $srcY - $y0;
for ($x = 0; $x < $dstW; $x++) {
$srcX = $x * $xRatio;
$x0 = (int) $srcX;
$x1 = min($x0 + 1, $srcW - 1);
$xFrac = $srcX - $x0;
for ($c = 0; $c < 4; $c++) {
$tl = $src[($y0 * $srcW + $x0) * 4 + $c];
$tr = $src[($y0 * $srcW + $x1) * 4 + $c];
$bl = $src[($y1 * $srcW + $x0) * 4 + $c];
$br = $src[($y1 * $srcW + $x1) * 4 + $c];
$top = $tl + ($tr - $tl) * $xFrac;
$bot = $bl + ($br - $bl) * $xFrac;
$dst[] = (int) round($top + ($bot - $top) * $yFrac);
}
}
}
return $dst;
}
/**
* Encode RGBA pixel array to PNG binary.
*/
private static function encodePng(array $pixels, int $w, int $h): string
{
// Build raw image data with filter byte 0 (None) per row
$raw = '';
for ($y = 0; $y < $h; $y++) {
$raw .= "\x00"; // filter: None
for ($x = 0; $x < $w; $x++) {
$i = ($y * $w + $x) * 4;
$raw .= chr($pixels[$i]) . chr($pixels[$i + 1]) . chr($pixels[$i + 2]) . chr($pixels[$i + 3]);
}
}
$compressed = gzcompress($raw);
// Build PNG
$png = "\x89PNG\r\n\x1a\n";
// IHDR
$ihdr = pack('NNCCCC', $w, $h, 8, 6, 0, 0, 0); // 8-bit RGBA
$png .= self::pngChunk('IHDR', $ihdr);
// IDAT
$png .= self::pngChunk('IDAT', $compressed);
// IEND
$png .= self::pngChunk('IEND', '');
return $png;
}
private static function pngChunk(string $type, string $data): string
{
$chunk = $type . $data;
return pack('N', strlen($data)) . $chunk . pack('N', crc32($chunk));
}
// ── Shared Utilities ────────────────────────────────────────────
/**
* Write ICO file from PNG data entries.
*/
private static function writeIco(array $entries, string $outPath): void
{
$count = count($entries); $count = count($entries);
$ico = pack('vvv', 0, 1, $count); $ico = pack('vvv', 0, 1, $count);
// Calculate offset: header (6) + directory entries (16 each)
$offset = 6 + ($count * 16); $offset = 6 + ($count * 16);
$imageData = ''; $imageData = '';
foreach ($entries as $entry) { foreach ($entries as $entry) {
$size = $entry['size'] >= 256 ? 0 : $entry['size']; $size = $entry['size'] >= 256 ? 0 : $entry['size'];
$dataLen = strlen($entry['data']); $dataLen = strlen($entry['data']);
// ICONDIRENTRY: width, height, colors, reserved, planes, bpp, size, offset
$ico .= pack('CCCCvvVV', $size, $size, 0, 0, 1, 32, $dataLen, $offset); $ico .= pack('CCCCvvVV', $size, $size, 0, 0, 1, 32, $dataLen, $offset);
$imageData .= $entry['data']; $imageData .= $entry['data'];
$offset += $dataLen; $offset += $dataLen;
@@ -474,6 +136,9 @@ class MokoFaviconHelper
file_put_contents($outPath, $ico . $imageData); file_put_contents($outPath, $ico . $imageData);
} }
/**
* Write a site.webmanifest for Android/PWA icon discovery.
*/
private static function generateManifest(string $outputDir): void private static function generateManifest(string $outputDir): void
{ {
$manifest = [ $manifest = [
@@ -488,9 +153,16 @@ class MokoFaviconHelper
); );
} }
/**
* Return the <link> tags to inject into <head>.
*
* @param string $basePath URL path to the favicon directory (relative to site root).
*
* @return string HTML link tags.
*/
public static function getHeadTags(string $basePath): string public static function getHeadTags(string $basePath): string
{ {
$basePath = htmlspecialchars(rtrim($basePath, '/'), ENT_QUOTES, 'UTF-8'); $basePath = rtrim($basePath, '/');
return '<link rel="apple-touch-icon" sizes="180x180" href="' . $basePath . '/apple-touch-icon.png">' . "\n" return '<link rel="apple-touch-icon" sizes="180x180" href="' . $basePath . '/apple-touch-icon.png">' . "\n"
. '<link rel="icon" type="image/png" sizes="32x32" href="' . $basePath . '/favicon-32x32.png">' . "\n" . '<link rel="icon" type="image/png" sizes="32x32" href="' . $basePath . '/favicon-32x32.png">' . "\n"
@@ -498,21 +170,4 @@ class MokoFaviconHelper
. '<link rel="manifest" href="' . $basePath . '/site.webmanifest">' . "\n" . '<link rel="manifest" href="' . $basePath . '/site.webmanifest">' . "\n"
. '<link rel="shortcut icon" href="' . $basePath . '/favicon.ico">' . "\n"; . '<link rel="shortcut icon" href="' . $basePath . '/favicon.ico">' . "\n";
} }
private static function log(string $message, string $priority = 'info'): void
{
$priorities = [
'info' => Log::INFO,
'warning' => Log::WARNING,
'error' => Log::ERROR,
];
Log::addLogger(
['text_file' => 'mokocassiopeia.log.php'],
Log::ALL,
['mokocassiopeia']
);
Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokocassiopeia');
}
} }

View File

@@ -22,9 +22,6 @@ class MokoMinifyHelper
*/ */
private const CSS_FILES = [ private const CSS_FILES = [
'css/template.css', 'css/template.css',
'css/offline.css',
'css/editor.css',
'css/a11y-high-contrast.css',
'css/theme/light.standard.css', 'css/theme/light.standard.css',
'css/theme/dark.standard.css', 'css/theme/dark.standard.css',
'css/theme/light.custom.css', 'css/theme/light.custom.css',

View File

@@ -26,14 +26,10 @@ $headerClass = htmlspecialchars($params->get('header_class', ''), ENT_COMPAT, 'U
<?php if ($module->showtitle) : ?> <?php if ($module->showtitle) : ?>
<<?php echo $headerTag; ?> class="mod-stats__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>> <<?php echo $headerTag; ?> class="mod-stats__title<?php echo $headerClass ? ' ' . $headerClass : ''; ?>"><?php echo $module->title; ?></<?php echo $headerTag; ?>>
<?php endif; ?> <?php endif; ?>
<table class="mod_stats__table"> <dl class="mod-stats__list">
<tbody>
<?php foreach ($list as $item) : ?> <?php foreach ($list as $item) : ?>
<tr> <dt class="mod-stats__label"><?php echo $item->title; ?></dt>
<th class="mod_stats__label" scope="row"><?php echo $item->title; ?></th> <dd class="mod-stats__data"><?php echo $item->data; ?></dd>
<td class="mod_stats__data"><?php echo $item->data; ?></td>
</tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </dl>
</table>
</div> </div>

View File

@@ -41,7 +41,7 @@ $params_favicon_source = (string) $this->params->get('favicon_source', '');
$params_theme_enabled = $this->params->get('theme_enabled', 1); $params_theme_enabled = $this->params->get('theme_enabled', 1);
$params_theme_control_type = (string) $this->params->get('theme_control_type', 'radios'); $params_theme_control_type = (string) $this->params->get('theme_control_type', 'radios');
$params_theme_fab_enabled = $this->params->get('theme_fab_enabled', 1); $params_theme_fab_enabled = $this->params->get('theme_fab_enabled', 1);
$params_theme_fab_pos = 'br'; $params_theme_fab_pos = $this->params->get('theme_fab_pos', 'br');
// Accessibility params // Accessibility params
$params_a11y_toolbar = $this->params->get('a11y_toolbar_enabled', 1); $params_a11y_toolbar = $this->params->get('a11y_toolbar_enabled', 1);
@@ -51,7 +51,7 @@ $params_a11y_contrast = $this->params->get('a11y_high_contrast', 1);
$params_a11y_links = $this->params->get('a11y_highlight_links', 1); $params_a11y_links = $this->params->get('a11y_highlight_links', 1);
$params_a11y_font = $this->params->get('a11y_readable_font', 1); $params_a11y_font = $this->params->get('a11y_readable_font', 1);
$params_a11y_animations = $this->params->get('a11y_pause_animations', 1); $params_a11y_animations = $this->params->get('a11y_pause_animations', 1);
$params_a11y_pos = 'br'; $params_a11y_pos = (string) $this->params->get('a11y_toolbar_pos', 'tl');
// Detecting Active Variables // Detecting Active Variables
$option = $input->getCmd('option', ''); $option = $input->getCmd('option', '');
@@ -71,27 +71,7 @@ $templatePath = 'media/templates/site/mokocassiopeia';
$faviconHeadTags = ''; $faviconHeadTags = '';
if ($params_favicon_source) { if ($params_favicon_source) {
require_once __DIR__ . '/helper/favicon.php'; require_once __DIR__ . '/helper/favicon.php';
// Joomla's media field returns paths like: $faviconSourceAbs = JPATH_ROOT . '/' . ltrim($params_favicon_source, '/');
// 'images/logo.png' (images folder)
// 'media/templates/site/mokocassiopeia/images/logo.png' (template media)
// 'logo.png' (bare filename)
// Strip Joomla's #joomlaImage:// fragment from media field value
$faviconSourceRel = strtok(ltrim($params_favicon_source, '/'), '#');
$faviconSourceAbs = JPATH_ROOT . '/' . $faviconSourceRel;
// Try common prefixes if not found
if (!is_file($faviconSourceAbs)) {
$candidates = [
JPATH_ROOT . '/images/' . $faviconSourceRel,
JPATH_ROOT . '/media/templates/site/' . $this->template . '/' . $faviconSourceRel,
JPATH_ROOT . '/media/templates/site/' . $this->template . '/images/' . basename($faviconSourceRel),
];
foreach ($candidates as $candidate) {
if (is_file($candidate)) {
$faviconSourceAbs = $candidate;
break;
}
}
}
$faviconOutputDir = JPATH_ROOT . '/images/favicons'; $faviconOutputDir = JPATH_ROOT . '/images/favicons';
$faviconUrlBase = Uri::root(true) . '/images/favicons'; $faviconUrlBase = Uri::root(true) . '/images/favicons';
@@ -426,7 +406,7 @@ $wa->useScript('user.js'); // js/user.js
</div> </div>
<?php if ($this->countModules('brand-aside', true)) : ?> <?php if ($this->countModules('brand-aside', true)) : ?>
<div class="container-brand-aside"> <div class="container-brand-aside">
<jdoc:include type="modules" name="brand-aside" style="card" /> <jdoc:include type="modules" name="brand-aside" style="none" />
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>

View File

@@ -17,7 +17,7 @@
"defgroup": "Joomla.Template.Site", "defgroup": "Joomla.Template.Site",
"ingroup": "MokoCassiopeia.Template.Assets", "ingroup": "MokoCassiopeia.Template.Assets",
"path": "./media/templates/site/mokocassiopeia/joomla.asset.json", "path": "./media/templates/site/mokocassiopeia/joomla.asset.json",
"version": "03.10.13", "version": "03.09.14",
"brief": "Joomla asset registry for MokoCassiopeia" "brief": "Joomla asset registry for MokoCassiopeia"
} }
}, },
@@ -34,18 +34,6 @@
"uri": "media/templates/site/mokocassiopeia/css/template.min.css", "uri": "media/templates/site/mokocassiopeia/css/template.min.css",
"attributes": {"media": "all"} "attributes": {"media": "all"}
}, },
{
"name": "template.offline",
"type": "style",
"uri": "media/templates/site/mokocassiopeia/css/offline.css",
"attributes": {"media": "all"}
},
{
"name": "template.offline.min",
"type": "style",
"uri": "media/templates/site/mokocassiopeia/css/offline.min.css",
"attributes": {"media": "all"}
},
{ {
"name": "template.user", "name": "template.user",
"type": "style", "type": "style",

View File

@@ -259,14 +259,16 @@ TPL_MOKOCASSIOPEIA_CSS_VARS_VM_DESC="<strong>Surfaces &amp; text</strong><br><co
TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL="Gable" TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL="Gable"
TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC="Colour tokens used by the Gable extension.<br><code>--gab-blue</code> — <code>#0066cc</code><br><code>--gab-green</code> — <code>#28a745</code><br><code>--gab-red</code> — <code>#dc3545</code><br><code>--gab-orange</code> — <code>#fd7e14</code><br><code>--gab-gray1</code> — <code>#495057</code><br><code>--gab-gray2</code> — <code>#6c757d</code><br><code>--gab-gray3</code> — <code>#adb5bd</code>" TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC="Colour tokens used by the Gable extension.<br><code>--gab-blue</code> — <code>#0066cc</code><br><code>--gab-green</code> — <code>#28a745</code><br><code>--gab-red</code> — <code>#dc3545</code><br><code>--gab-orange</code> — <code>#fd7e14</code><br><code>--gab-gray1</code> — <code>#495057</code><br><code>--gab-gray2</code> — <code>#6c757d</code><br><code>--gab-gray3</code> — <code>#adb5bd</code>"
TPL_MOKOCASSIOPEIA_CSS_VARS_FOOTER_LABEL="Footer"
TPL_MOKOCASSIOPEIA_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>)"
; ===== Theme Preview tab ===== ; ===== Theme Preview tab =====
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview" TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview"
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colours, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokocassiopeia/templates/theme-test.html</code>.</p>" TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colours, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokocassiopeia/templates/theme-test.html</code>.</p>"
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME="<iframe src='../templates/mokocassiopeia/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>" TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME="<iframe src='../templates/mokocassiopeia/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>"
; ===== Brand Showcase tab =====
TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_FIELDSET_LABEL="Brand Showcase"
TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_INTRO="<p>Interactive brand and Bootstrap 5 component showcase with colour system gradients. <strong>Hover over any gradient</strong> to sample the exact pixel colour at that point. Use the <strong>Toggle Light / Dark</strong> button to switch themes. This page is also available standalone at <code>templates/mokocassiopeia/templates/brand-showcase.html</code>.</p>"
TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_FRAME="<iframe src='../templates/mokocassiopeia/templates/brand-showcase.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Brand and Bootstrap showcase with colour sampler'></iframe>"
; ===== Misc ===== ; ===== Misc =====
MOD_BREADCRUMBS_HERE="YOU ARE HERE:" MOD_BREADCRUMBS_HERE="YOU ARE HERE:"

View File

@@ -259,14 +259,16 @@ TPL_MOKOCASSIOPEIA_CSS_VARS_VM_DESC="<strong>Surfaces &amp; text</strong><br><co
TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL="Gable" TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL="Gable"
TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC="Color tokens used by the Gable extension.<br><code>--gab-blue</code> — <code>#0066cc</code><br><code>--gab-green</code> — <code>#28a745</code><br><code>--gab-red</code> — <code>#dc3545</code><br><code>--gab-orange</code> — <code>#fd7e14</code><br><code>--gab-gray1</code> — <code>#495057</code><br><code>--gab-gray2</code> — <code>#6c757d</code><br><code>--gab-gray3</code> — <code>#adb5bd</code>" TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC="Color tokens used by the Gable extension.<br><code>--gab-blue</code> — <code>#0066cc</code><br><code>--gab-green</code> — <code>#28a745</code><br><code>--gab-red</code> — <code>#dc3545</code><br><code>--gab-orange</code> — <code>#fd7e14</code><br><code>--gab-gray1</code> — <code>#495057</code><br><code>--gab-gray2</code> — <code>#6c757d</code><br><code>--gab-gray3</code> — <code>#adb5bd</code>"
TPL_MOKOCASSIOPEIA_CSS_VARS_FOOTER_LABEL="Footer"
TPL_MOKOCASSIOPEIA_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>)"
; ===== Theme Preview tab ===== ; ===== Theme Preview tab =====
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview" TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FIELDSET_LABEL="Theme Preview"
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colors, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokocassiopeia/templates/theme-test.html</code>.</p>" TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO="<p>Live preview of all CSS variables, hero variants, block colors, and Bootstrap components rendered with your active theme. Use the <strong>Toggle Light / Dark</strong> button inside the preview to switch modes. This page is also available as a standalone file at <code>templates/mokocassiopeia/templates/theme-test.html</code>.</p>"
TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME="<iframe src='../templates/mokocassiopeia/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>" TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME="<iframe src='../templates/mokocassiopeia/templates/theme-test.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Theme test sheet preview'></iframe>"
; ===== Brand Showcase tab =====
TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_FIELDSET_LABEL="Brand Showcase"
TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_INTRO="<p>Interactive brand and Bootstrap 5 component showcase with color system gradients. <strong>Hover over any gradient</strong> to sample the exact pixel color at that point. Use the <strong>Toggle Light / Dark</strong> button to switch themes. This page is also available standalone at <code>templates/mokocassiopeia/templates/brand-showcase.html</code>.</p>"
TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_FRAME="<iframe src='../templates/mokocassiopeia/templates/brand-showcase.html' style='width:100%;height:80vh;border:1px solid #dee2e6;border-radius:.375rem;' loading='lazy' title='Brand and Bootstrap showcase with color sampler'></iframe>"
; ===== Misc ===== ; ===== Misc =====
MOD_BREADCRUMBS_HERE="YOU ARE HERE:" MOD_BREADCRUMBS_HERE="YOU ARE HERE:"

View File

@@ -1,258 +0,0 @@
/* 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
*/
/* === Offline Page — Full-viewport background with centered overlay card === */
.moko-offline-wrap {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
color: #fff;
font-family: var(--body-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
/* Background: offline_image set inline, or fall back to header background */
background-color: var(--color-primary, #112855);
background-image: var(--header-background-image, none);
background-position: var(--header-background-position, center);
background-attachment: var(--header-background-attachment, fixed);
background-repeat: no-repeat;
background-size: cover;
}
/* Dark theme: overlay to darken the background */
:root[data-bs-theme="dark"] .moko-offline-wrap {
position: relative;
}
:root[data-bs-theme="dark"] .moko-offline-wrap::before {
content: "";
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 0;
}
/* === Centered Card Overlay === */
.moko-offline-card {
width: 100%;
max-width: 720px;
background: var(--offline-card-bg, rgba(0, 0, 0, 0.6));
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-radius: 0.875rem;
padding: 2.5rem 2rem;
text-align: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
@media (min-width: 768px) {
.moko-offline-card {
padding: 3rem;
}
}
@media (max-width: 575.98px) {
.moko-offline-wrap {
padding: 1rem 0.75rem;
}
.moko-offline-card {
padding: 2rem 1.25rem;
}
}
/* === Logo header area === */
.moko-offline-brand {
display: block;
text-align: center;
text-decoration: none;
color: #fff;
margin-bottom: 1.5rem;
}
.moko-offline-brand:hover {
color: var(--accent-color-primary, #3f8ff0);
}
.moko-offline-brand img {
max-width: 100%;
height: auto;
}
.moko-offline-brand .site-title {
display: block;
font-size: 2rem;
font-weight: 700;
font-family: 'Osaka', var(--body-font-family, sans-serif);
color: var(--accent-color-secondary, #6fb3ff);
}
.moko-offline-brand .brand-tagline {
display: block;
opacity: 0.7;
font-size: 0.9rem;
margin-top: 0.25rem;
}
/* === Offline Message === */
.moko-offline-message {
margin-bottom: 1.5rem;
}
.moko-offline-message h1 {
font-size: 1.5rem;
font-weight: 700;
color: #fff;
margin-bottom: 0.5rem;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.moko-offline-message p {
color: rgba(255, 255, 255, 0.85);
line-height: 1.6;
margin: 0;
}
/* === Offline Module Position === */
.moko-offline-modules {
margin-bottom: 1.5rem;
text-align: left;
}
/* === Copyright Footer === */
.moko-offline-copyright {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.45);
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.moko-offline-copyright a {
color: rgba(255, 255, 255, 0.6);
text-decoration: underline;
}
.moko-offline-copyright a:hover {
color: #fff;
}
/* === Login Accordion (translucent on overlay) === */
.moko-offline-card .accordion {
text-align: left;
}
.moko-offline-card .accordion-item {
background: transparent;
border-color: rgba(255, 255, 255, 0.15);
}
.moko-offline-card .accordion-button {
background: transparent;
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
padding: 0.75rem 1rem;
}
.moko-offline-card .accordion-button:not(.collapsed) {
background: rgba(255, 255, 255, 0.05);
color: #fff;
box-shadow: none;
}
.moko-offline-card .accordion-button::after {
filter: invert(1) brightness(2);
}
.moko-offline-card .accordion-body {
background: transparent;
padding: 1rem;
}
/* === Form Controls (glass effect) === */
.moko-offline-card .form-control {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
color: #fff;
}
.moko-offline-card .form-control::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.moko-offline-card .form-control:focus {
background-color: rgba(255, 255, 255, 0.15);
border-color: var(--accent-color-primary, #3f8ff0);
color: #fff;
box-shadow: 0 0 0 0.25rem rgba(63, 143, 240, 0.25);
}
.moko-offline-card .form-label {
color: rgba(255, 255, 255, 0.8);
font-size: 0.875rem;
}
.moko-offline-card .form-check-label {
color: rgba(255, 255, 255, 0.7);
}
.moko-offline-card .form-check-input {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.3);
}
.moko-offline-card .form-check-input:checked {
background-color: var(--accent-color-primary, #3f8ff0);
border-color: var(--accent-color-primary, #3f8ff0);
}
/* === Button === */
.moko-offline-card .btn-primary {
background-color: var(--color-primary, #112855);
border-color: rgba(255, 255, 255, 0.15);
color: #fff;
}
.moko-offline-card .btn-primary:hover {
background-color: var(--accent-color-primary, #3f8ff0);
border-color: var(--accent-color-primary, #3f8ff0);
}
/* === Links === */
.moko-offline-card a {
color: var(--accent-color-primary, #3f8ff0);
}
.moko-offline-card a:hover {
color: #fff;
}
/* === Joomla system messages === */
.moko-offline-messages {
width: 100%;
max-width: 720px;
margin-bottom: 1rem;
}
/* === Skip Link === */
.skip-link {
position: absolute;
left: -9999px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.skip-link:focus {
position: static;
width: auto;
height: auto;
padding: 0.5rem 1rem;
}

View File

@@ -2593,8 +2593,8 @@ progress {
font-size: 1rem; font-size: 1rem;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
color: var(--input-color, #1a2332); color: var(--input-color, #e6ebf1);
background-color: var(--input-bg, #e6ebf1); background-color: var(--input-bg, #1a2332);
background-clip: padding-box; background-clip: padding-box;
border: 1px solid var(--input-border-color, #3a4250); border: 1px solid var(--input-border-color, #3a4250);
-webkit-appearance: none; -webkit-appearance: none;
@@ -13912,7 +13912,7 @@ meter {
height: 4px; height: 4px;
margin: 1rem auto 2rem; margin: 1rem auto 2rem;
content: ""; content: "";
background: var(--body-bg, #e6ebf1); background: var(--body-color, #e6ebf1);
} }
.container-banner .banner-overlay .overlay .text-thin .lead { .container-banner .banner-overlay .overlay .text-thin .lead {
@@ -14099,7 +14099,7 @@ td .form-control {
margin: 0.5em; margin: 0.5em;
color: hsl(0, 0%, 0%); color: hsl(0, 0%, 0%);
text-align: start; text-align: start;
background: var(--body-bg, #e6ebf1); background: var(--body-color, #e6ebf1);
border: 1px solid hsl(210, 7%, 46%); border: 1px solid hsl(210, 7%, 46%);
border-radius: 0.25rem; border-radius: 0.25rem;
-webkit-box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.8); -webkit-box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.8);
@@ -14233,28 +14233,6 @@ fieldset>* {
margin-inline-start: auto; margin-inline-start: auto;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1em;
}
.container-brand-aside>* {
flex: 1;
margin: 0.5em 0;
}
@media (max-width: 991.98px) {
.header-brand-wrap {
flex-direction: column;
align-items: stretch;
}
.container-brand-aside {
margin-inline-start: 0;
flex-direction: column;
}
.container-brand-aside>* {
flex: 0 1 auto;
}
} }
.container-header .navbar-brand { .container-header .navbar-brand {
@@ -15549,7 +15527,7 @@ joomla-alert {
min-height: 43px; min-height: 43px;
padding: 0.25rem; padding: 0.25rem;
color: var(--subhead-color, #9fa6ad); color: var(--subhead-color, #9fa6ad);
background: var(--body-bg, #e6ebf1); background: var(--body-color, #e6ebf1);
-webkit-box-shadow: -3px -2px 22px var(--box-shadow-gray, #1a2027); -webkit-box-shadow: -3px -2px 22px var(--box-shadow-gray, #1a2027);
box-shadow: -3px -2px 22px var(--box-shadow-gray, #1a2027); box-shadow: -3px -2px 22px var(--box-shadow-gray, #1a2027);
} }
@@ -15591,7 +15569,7 @@ joomla-alert {
font-size: 1rem; font-size: 1rem;
line-height: 2.45rem; line-height: 2.45rem;
color: var(--subhead-color, #9fa6ad); color: var(--subhead-color, #9fa6ad);
background: var(--body-bg, #e6ebf1); background: var(--body-color, #e6ebf1);
border-color: hsl(210, 11%, 71%); border-color: hsl(210, 11%, 71%);
} }
@@ -15793,13 +15771,7 @@ body.wrapper-fluid header>.grid-child {
} }
footer .grid-child>div { footer .grid-child>div {
padding: calc(var(--navbar-padding-y, 1rem) * 3) padding: var(--navbar-padding-y, 1rem) var(--navbar-padding-x, 1rem) 0;
calc(var(--navbar-padding-x, 1rem) * 1)
0;
}
.mod-footer {
border-top: 1px solid var(--border-gray, #b2bfcds);
} }
header .grid-child .navbar-brand { header .grid-child .navbar-brand {
@@ -16330,7 +16302,7 @@ body:not(.has-sidebar-right) .site-grid .container-component {
.nav-tabs+.tab-content { .nav-tabs+.tab-content {
padding: 0.9375rem; padding: 0.9375rem;
background: var(--body-bg, #e6ebf1); background: var(--body-color, #e6ebf1);
border: 1px solid; border: 1px solid;
border-color: hsl(210, 14%, 89%); border-color: hsl(210, 14%, 89%);
border-radius: 0 0 0.25rem 0.25rem; border-radius: 0 0 0.25rem 0.25rem;
@@ -16405,7 +16377,7 @@ body:not(.has-sidebar-right) .site-grid .container-component {
} }
.chosen-container.chosen-container-single .chosen-drop { .chosen-container.chosen-container-single .chosen-drop {
background: var(--body-bg, #e6ebf1); background: var(--body-color, #e6ebf1);
border: 1px solid hsl(210, 14%, 83%); border: 1px solid hsl(210, 14%, 83%);
} }
@@ -17092,20 +17064,14 @@ form .form-select {
display: flex; display: flex;
align-items: center; align-items: center;
gap: .5rem; gap: .5rem;
padding: .5rem .75rem; padding: calc(var(--padding-x, 0.25rem) * 2) calc(var(--padding-y, 0.25rem) * 3) calc(var(--padding-x, 0.25rem) * 2) calc(var(--padding-y, 0.25rem) * 8);
border-radius: 999px; border-radius: 999px;
border: 2px solid var(--theme-fab-border, rgba(255,255,255,.3)); border: none;
background: var(--theme-fab-bg, var(--color-primary, #112855)); background: var(--muted-color, #6d757e);
color: var(--theme-fab-color, #fff); box-shadow: var(--box-shadow, 0 .5rem 1rem #00000066);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1);
font: inherit; font: inherit;
color: #fff;
font-weight: 600; font-weight: 600;
transition: transform .15s, box-shadow .15s;
}
#mokoThemeFab:hover {
transform: scale(1.05);
box-shadow: 0 6px 28px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.15);
} }
#mokoThemeFab.pos-br { #mokoThemeFab.pos-br {
@@ -17128,47 +17094,50 @@ form .form-select {
top: 1rem; top: 1rem;
} }
/* Sun/Moon theme toggle button */ #mokoThemeFab .switch {
.theme-icon-btn { display: inline-flex;
display: flex;
align-items: center; align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: none;
border-radius: 50%;
background: var(--theme-fab-btn-bg, rgba(255,255,255,.15));
color: inherit;
font-size: 1.25rem;
cursor: pointer;
padding: 0;
position: relative; position: relative;
width: 44px;
height: 24px;
background: var(--secondary-color, #e6ebf1bf);
transition: background .2s, border-color .2s;
border-radius: var(--border-radius-xxl, 2rem);
} }
.theme-icon-btn .fa-sun, #mokoThemeFab .knob {
.theme-icon-btn .fa-moon {
position: absolute; position: absolute;
transition: opacity .2s, transform .2s; top: 2px;
left: 2px;
width: 20px;
height: 20px;
border-radius: var(--border-radius-xxl, 2rem);
background: var(--bs-body-bg, #fff);
box-shadow: var(--box-shadow, 0 .5rem 1rem #00000066);
transition: transform .2s ease;
} }
/* Light mode: show sun, hide moon */ #mokoThemeFab [role="switch"][aria-checked="true"] .knob {
.theme-icon-btn.is-light .fa-sun { transform: translateX(20px);
opacity: 1;
transform: rotate(0deg);
}
.theme-icon-btn.is-light .fa-moon {
opacity: 0;
transform: rotate(-90deg);
} }
/* Dark mode: show moon, hide sun */ #mokoThemeFab [role="switch"][aria-checked="true"] .switch {
.theme-icon-btn.is-dark .fa-moon { background: rgba(var(--secondary-color, #e6ebf1bf), .15);
opacity: 1;
transform: rotate(0deg);
} }
.theme-icon-btn.is-dark .fa-sun {
opacity: 0; button#mokoThemeSwitch {
transform: rotate(90deg); border: unset;
background-color: unset;
}
#mokoThemeFab .label {
user-select: none;
font-size: .875rem;
color: #fff;
}
#mokoThemeFab button {
color: #fff;
} }
/* Auto toggle switch (on/off style) */ /* Auto toggle switch (on/off style) */
@@ -17195,14 +17164,14 @@ form .form-select {
height: 18px; height: 18px;
border: none; border: none;
border-radius: 999px; border-radius: 999px;
background: var(--danger, #c23a31); background: var(--secondary-color, #6c757d);
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
transition: background .2s; transition: background .2s;
} }
.auto-switch.on { .auto-switch.on {
background: var(--success, #4aa664); background: var(--link-color, #3565e5);
} }
.auto-track { .auto-track {
@@ -17238,15 +17207,6 @@ form .form-select {
} }
/* Inline a11y toggle inside theme FAB */ /* Inline a11y toggle inside theme FAB */
/* Light mode: darker blue */
:root[data-bs-theme="light"] .a11y-toggle-inline {
--a11y-btn-bg: #1565c0;
}
/* Dark mode: lighter blue */
:root[data-bs-theme="dark"] .a11y-toggle-inline {
--a11y-btn-bg: #42a5f5;
}
.a11y-toggle-inline { .a11y-toggle-inline {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -17254,16 +17214,25 @@ form .form-select {
width: 28px; width: 28px;
height: 28px; height: 28px;
border-radius: 50%; border-radius: 50%;
border: none; border: 1.5px solid currentColor;
background: var(--a11y-btn-bg, #1976d2); background: transparent;
color: #fff; color: inherit;
font-size: 1rem; font-size: 1rem;
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
transition: background .2s, color .2s;
opacity: .8;
}
.a11y-toggle-inline:hover,
.a11y-toggle-inline:focus-visible {
opacity: 1;
background: rgba(255,255,255,.15);
} }
.a11y-toggle-inline.active { .a11y-toggle-inline.active {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--a11y-btn-bg, #1976d2); opacity: 1;
background: rgba(255,255,255,.25);
} }
/* Floating a11y panel when inline */ /* Floating a11y panel when inline */
@@ -17392,6 +17361,36 @@ body.site.error-page {
text-decoration: none; text-decoration: none;
} }
#mokoThemeFab .knob {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
border-radius: var(--border-radius-xxl, 2rem);
background: var(--bs-body-bg, #fff);
box-shadow: var(--box-shadow, 0 .5rem 1rem #00000066);
transition: transform .2s ease;
}
#mokoThemeFab [role="switch"][aria-checked="true"] .knob {
transform: translateX(20px);
}
#mokoThemeFab [role="switch"][aria-checked="true"] .switch {
background: rgba(var(--secondary-color, #e6ebf1bf), .15);
}
button#mokoThemeSwitch {
border: unset;
background-color: unset;
}
#mokoThemeFab .label {
user-select: none;
font-size: .875rem;
}
#mokoThemeFab.debug-outline { #mokoThemeFab.debug-outline {
outline: 2px dashed var(--pink, #ff8fc0); outline: 2px dashed var(--pink, #ff8fc0);
outline-offset: 2px; outline-offset: 2px;
@@ -17500,13 +17499,12 @@ body[data-theme-fab-enabled="1"] #mokoA11yToolbar {
/* Panel */ /* Panel */
.a11y-panel { .a11y-panel {
background: var(--body-bg, var(--bs-body-bg, #fff)); background: var(--bs-body-bg, #fff);
border: 1px solid var(--border-color, #dee2e6); border: 1px solid var(--border-color, #dee2e6);
border-radius: var(--border-radius, .375rem); border-radius: var(--border-radius, .375rem);
padding: .75rem; padding: .75rem;
min-width: 200px; min-width: 200px;
box-shadow: var(--box-shadow-lg, 0 1rem 3rem rgba(0,0,0,.175)); box-shadow: var(--box-shadow-lg, 0 1rem 3rem rgba(0,0,0,.175));
color: var(--body-font-color, var(--body-color, #e6ebf1));
} }
.a11y-group { .a11y-group {
@@ -17542,8 +17540,8 @@ body[data-theme-fab-enabled="1"] #mokoA11yToolbar {
height: 34px; height: 34px;
border: 1px solid var(--border-color, #dee2e6); border: 1px solid var(--border-color, #dee2e6);
border-radius: var(--border-radius, .375rem); border-radius: var(--border-radius, .375rem);
background: var(--secondary-bg, var(--bs-body-bg, #fff)); background: var(--bs-body-bg, #fff);
color: var(--body-font-color, var(--body-color, #e6ebf1)); color: var(--body-font-color, #444);
font-size: .875rem; font-size: .875rem;
cursor: pointer; cursor: pointer;
transition: background .15s, border-color .15s; transition: background .15s, border-color .15s;
@@ -17561,7 +17559,7 @@ body[data-theme-fab-enabled="1"] #mokoA11yToolbar {
font-weight: 600; font-weight: 600;
min-width: 3ch; min-width: 3ch;
text-align: center; text-align: center;
color: var(--body-font-color, var(--body-color, #e6ebf1)); color: var(--body-font-color, #444);
} }
.a11y-btn-wide { .a11y-btn-wide {
@@ -18688,7 +18686,7 @@ nav[data-toggle=toc] .nav-link.active+ul{
flex: 0 0 auto; flex: 0 0 auto;
background-color: var(--color-primary, #112855); background-color: var(--color-primary, #112855);
color: var(--mainmenu-nav-link-color, #fff); color: var(--mainmenu-nav-link-color, #fff);
border: 1px solid var(--input-border-color, #3a4250); border-color: var(--color-primary, #112855);
padding: 0.6rem 1rem; padding: 0.6rem 1rem;
border-radius: 0 0.25rem 0.25rem 0; border-radius: 0 0.25rem 0.25rem 0;
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
@@ -18697,7 +18695,7 @@ nav[data-toggle=toc] .nav-link.active+ul{
.mod-finder__search.input-group button:hover, .mod-finder__search.input-group button:hover,
.container-search button[type="submit"]:hover { .container-search button[type="submit"]:hover {
background-color: var(--color-hover, gray); background-color: var(--color-hover, gray);
border-color: var(--input-border-color, #3a4250); border-color: var(--color-hover, gray);
} }
.mod-finder__search.input-group button:focus, .mod-finder__search.input-group button:focus,
@@ -21666,33 +21664,6 @@ nav[data-toggle=toc] .nav-link.active+ul{
color: var(--gray-600, #48525d); color: var(--gray-600, #48525d);
} }
/* === mod_stats === */
.mod_stats__table {
width: 100%;
border-collapse: collapse;
}
.mod_stats__table tr {
border-bottom: 1px solid var(--border-color, #2b323b);
}
.mod_stats__table tr:last-child {
border-bottom: none;
}
.mod_stats__label {
text-align: start;
font-weight: 600;
padding: 0.6rem 1rem 0.6rem 0;
color: var(--body-font-color, #e6ebf1);
}
.mod_stats__data {
text-align: end;
padding: 0.6rem 0;
color: var(--gray-600, #48525d);
}
/* === Mobile Responsive Adjustments === */ /* === Mobile Responsive Adjustments === */
@media (max-width: 575.98px) { @media (max-width: 575.98px) {
.mod-kunena-login__input { .mod-kunena-login__input {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,6 @@
<svg <svg
width="800" width="800"
height="400" height="400"
viewBox="0 0 800 400"
id="svg2" id="svg2"
version="1.1" version="1.1"
sodipodi:docname="bg.svg" sodipodi:docname="bg.svg"
@@ -94,14 +93,14 @@
style="display:inline;fill:#e5e5e5;fill-opacity:1;stroke:none;stroke-width:2.20303" style="display:inline;fill:#e5e5e5;fill-opacity:1;stroke:none;stroke-width:2.20303"
id="rect4741" id="rect4741"
width="800" width="800"
height="494" height="400"
x="0" x="0"
y="46.331768" /> y="46.331768" />
<rect <rect
style="display:inline;fill:url(#pattern4758);fill-opacity:1;stroke:none;stroke-width:2.20303" style="display:inline;fill:url(#pattern4758);fill-opacity:1;stroke:none;stroke-width:2.20303"
id="rect4737" id="rect4737"
width="800" width="800"
height="494" height="400"
x="0" x="0"
y="46.699127" /> y="46.699127" />
</g> </g>

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -62,33 +62,30 @@
wrap.id = 'mokoThemeFab'; wrap.id = 'mokoThemeFab';
wrap.className = posClassFromBody(); wrap.className = posClassFromBody();
// Sun/Moon toggle button // Light label
var lblL = doc.createElement('span');
lblL.className = 'label';
lblL.textContent = 'Light';
// Switch
var switchWrap = doc.createElement('button'); var switchWrap = doc.createElement('button');
switchWrap.id = 'mokoThemeSwitch'; switchWrap.id = 'mokoThemeSwitch';
switchWrap.type = 'button'; switchWrap.type = 'button';
switchWrap.className = 'theme-icon-btn'; switchWrap.setAttribute('role', 'switch');
switchWrap.setAttribute('aria-label', 'Toggle dark mode'); switchWrap.setAttribute('aria-label', 'Toggle dark mode');
switchWrap.setAttribute('aria-checked', 'false');
var sunIcon = doc.createElement('i'); var track = doc.createElement('span');
sunIcon.className = 'fa-solid fa-sun'; track.className = 'switch';
sunIcon.setAttribute('aria-hidden', 'true'); var knob = doc.createElement('span');
knob.className = 'knob';
track.appendChild(knob);
switchWrap.appendChild(track);
var moonIcon = doc.createElement('i'); // Dark label
moonIcon.className = 'fa-solid fa-moon'; var lblD = doc.createElement('span');
moonIcon.setAttribute('aria-hidden', 'true'); lblD.className = 'label';
lblD.textContent = 'Dark';
switchWrap.appendChild(sunIcon);
switchWrap.appendChild(moonIcon);
function updateThemeIcon(theme) {
if (theme === 'dark') {
switchWrap.classList.add('is-dark');
switchWrap.classList.remove('is-light');
} else {
switchWrap.classList.add('is-light');
switchWrap.classList.remove('is-dark');
}
}
// Auto toggle (on/off switch style) // Auto toggle (on/off switch style)
var autoWrap = doc.createElement('div'); var autoWrap = doc.createElement('div');
@@ -130,7 +127,7 @@
var current = (root.getAttribute('data-bs-theme') || 'light').toLowerCase(); var current = (root.getAttribute('data-bs-theme') || 'light').toLowerCase();
var next = current === 'dark' ? 'light' : 'dark'; var next = current === 'dark' ? 'light' : 'dark';
applyTheme(next); applyTheme(next);
updateThemeIcon(next); switchWrap.setAttribute('aria-checked', next === 'dark' ? 'true' : 'false');
// Turn off auto when manually switching // Turn off auto when manually switching
auto.classList.remove('on'); auto.classList.remove('on');
auto.setAttribute('aria-checked', 'false'); auto.setAttribute('aria-checked', 'false');
@@ -148,7 +145,7 @@
clearStored(); clearStored();
var sys = systemTheme(); var sys = systemTheme();
applyTheme(sys); applyTheme(sys);
updateThemeIcon(sys); switchWrap.setAttribute('aria-checked', sys === 'dark' ? 'true' : 'false');
} }
}); });
@@ -157,7 +154,7 @@
if (!getStored()) { if (!getStored()) {
var sys = systemTheme(); var sys = systemTheme();
applyTheme(sys); applyTheme(sys);
updateThemeIcon(sys); switchWrap.setAttribute('aria-checked', sys === 'dark' ? 'true' : 'false');
} }
}; };
if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onMql); if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onMql);
@@ -165,10 +162,12 @@
// Initial state // Initial state
var initial = getStored() || systemTheme(); var initial = getStored() || systemTheme();
updateThemeIcon(initial); switchWrap.setAttribute('aria-checked', initial === 'dark' ? 'true' : 'false');
// Mount // Mount
wrap.appendChild(lblL);
wrap.appendChild(switchWrap); wrap.appendChild(switchWrap);
wrap.appendChild(lblD);
wrap.appendChild(autoWrap); wrap.appendChild(autoWrap);
wrap.appendChild(divider); wrap.appendChild(divider);
wrap.appendChild(a11ySlot); wrap.appendChild(a11ySlot);
@@ -293,17 +292,7 @@
toggle.className = "a11y-toggle"; toggle.className = "a11y-toggle";
toggle.setAttribute("aria-label", "Accessibility options"); toggle.setAttribute("aria-label", "Accessibility options");
toggle.setAttribute("aria-expanded", "false"); toggle.setAttribute("aria-expanded", "false");
var a11yIcon = faIcon("fa-solid fa-universal-access"); toggle.appendChild(faIcon("fa-solid fa-universal-access"));
// Unicode fallback if FA7 glyph doesn't render (e.g. FA6/FA7 conflict)
setTimeout(function () {
var cs = win.getComputedStyle(a11yIcon, "::before");
if (!cs.content || cs.content === "none" || cs.content === '""' || cs.content === '"" / ""') {
a11yIcon.className = "";
a11yIcon.textContent = "\u267F";
a11yIcon.style.fontSize = "1.1rem";
}
}, 500);
toggle.appendChild(a11yIcon);
// Panel // Panel
var panel = doc.createElement("div"); var panel = doc.createElement("div");
@@ -641,154 +630,6 @@
}); });
} }
// ========================================================================
// CSS VARIABLE CLICK-TO-COPY
// ========================================================================
/**
* Inject toast + variable-chip styles once.
*/
function injectVarCopyStyles() {
if (doc.getElementById("moko-var-copy-styles")) return;
var style = doc.createElement("style");
style.id = "moko-var-copy-styles";
style.textContent =
".moko-var-chip{cursor:pointer;font-family:var(--font-monospace,monospace);font-size:.875em;" +
"background:var(--secondary-bg,#151b22);color:var(--link-color,#8ab4f8);" +
"border:1px solid var(--border-color,#2b323b);border-radius:.25rem;padding:.1em .4em;" +
"transition:background .15s,border-color .15s;white-space:nowrap;display:inline}" +
".moko-var-chip:hover{background:var(--color-primary,#112855);color:#fff;border-color:var(--color-primary,#112855)}" +
".moko-toast{position:fixed;bottom:1.5rem;left:50%;transform:translateX(-50%);z-index:10000;" +
"background:var(--color-primary,#112855);color:#fff;padding:.6rem 1.25rem;" +
"border-radius:.375rem;font-size:.875rem;box-shadow:0 4px 12px rgba(0,0,0,.25);" +
"opacity:0;transition:opacity .2s;pointer-events:none}" +
".moko-toast--show{opacity:1}";
doc.head.appendChild(style);
}
/**
* Show a brief "Copied to clipboard" toast.
* @param {string} text - The variable name that was copied
*/
function showCopyToast(text) {
var existing = doc.querySelector(".moko-toast");
if (existing) existing.remove();
var toast = doc.createElement("div");
toast.className = "moko-toast";
toast.textContent = "Copied to clipboard: " + text;
doc.body.appendChild(toast);
// Trigger reflow then show
void toast.offsetWidth;
toast.classList.add("moko-toast--show");
setTimeout(function () {
toast.classList.remove("moko-toast--show");
setTimeout(function () { toast.remove(); }, 200);
}, 2000);
}
/**
* Copy text to clipboard and show toast.
* @param {string} text
*/
function copyVariable(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
showCopyToast(text);
});
} else {
// Fallback for older browsers using deprecated API
var ta = doc.createElement("textarea");
ta.value = text;
ta.style.cssText = "position:fixed;left:-9999px";
doc.body.appendChild(ta);
ta.select();
try { doc.execCommand("copy"); } catch (e) { /* noop */ }
ta.remove();
showCopyToast(text);
}
}
/**
* Scan text nodes for CSS variable patterns (--variable-name) and wrap
* each match in a clickable chip that copies the variable to clipboard.
*/
function initVarCopy() {
injectVarCopyStyles();
// Pattern: --[a-zA-Z] followed by word/hyphen chars
var varPattern = /--[a-zA-Z][\w-]*/g;
// Elements to skip (inputs, scripts, styles, already-processed, code editors)
var SKIP_TAGS = { SCRIPT: 1, STYLE: 1, TEXTAREA: 1, INPUT: 1, SELECT: 1, NOSCRIPT: 1 };
var walker = doc.createTreeWalker(
doc.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: function (node) {
if (SKIP_TAGS[node.parentNode.tagName]) return NodeFilter.FILTER_REJECT;
if (node.parentNode.classList && node.parentNode.classList.contains("moko-var-chip")) return NodeFilter.FILTER_REJECT;
if (!varPattern.test(node.nodeValue)) return NodeFilter.FILTER_REJECT;
varPattern.lastIndex = 0;
return NodeFilter.FILTER_ACCEPT;
}
}
);
var textNodes = [];
while (walker.nextNode()) textNodes.push(walker.currentNode);
textNodes.forEach(function (node) {
var text = node.nodeValue;
var frag = doc.createDocumentFragment();
var lastIndex = 0;
var match;
varPattern.lastIndex = 0;
while ((match = varPattern.exec(text)) !== null) {
// Text before the match
if (match.index > lastIndex) {
frag.appendChild(doc.createTextNode(text.slice(lastIndex, match.index)));
}
// Clickable chip
var chip = doc.createElement("span");
chip.className = "moko-var-chip";
chip.textContent = match[0];
chip.setAttribute("role", "button");
chip.setAttribute("tabindex", "0");
chip.setAttribute("title", "Click to copy " + match[0]);
chip.addEventListener("click", (function (varName) {
return function (e) {
e.preventDefault();
copyVariable(varName);
};
})(match[0]));
chip.addEventListener("keydown", (function (varName) {
return function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
copyVariable(varName);
}
};
})(match[0]));
frag.appendChild(chip);
lastIndex = match.index + match[0].length;
}
// Remaining text after last match
if (lastIndex < text.length) {
frag.appendChild(doc.createTextNode(text.slice(lastIndex)));
}
node.parentNode.replaceChild(frag, node);
});
}
/** /**
* Run all template JS initializations * Run all template JS initializations
*/ */
@@ -815,7 +656,6 @@
initBackTop(); initBackTop();
initSearchToggle(); initSearchToggle();
initSidebarAccordion(); initSidebarAccordion();
initVarCopy();
} }
if (doc.readyState === "loading") { if (doc.readyState === "loading") {

View File

@@ -1,5 +1,5 @@
<?php <?php
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> /* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project. This file is part of a Moko Consulting project.
@@ -26,90 +26,59 @@ use Joomla\CMS\Uri\Uri;
$app = Factory::getApplication(); $app = Factory::getApplication();
$doc = Factory::getDocument(); $doc = Factory::getDocument();
$wa = $doc->getWebAssetManager();
$params = $this->params ?: $app->getTemplate(true)->params; $params = $this->params ?: $app->getTemplate(true)->params;
$direction = $this->direction ?: 'ltr'; $direction = $this->direction ?: 'ltr';
// Register the template's asset manifest (not auto-loaded in offline context)
$manifestPath = JPATH_ROOT . '/media/templates/site/' . $this->template . '/joomla.asset.json';
if (is_file($manifestPath)) {
$wa->getRegistry()->addRegistryFile($manifestPath);
}
// Load language files (not auto-loaded in offline context)
$lang = Factory::getLanguage();
$lang->load('tpl_' . $this->template, JPATH_ROOT . '/templates/' . $this->template);
$lang->load('tpl_' . $this->template, JPATH_ROOT);
$lang->load('com_users', JPATH_ROOT);
$lang->load('com_users', JPATH_ROOT . '/components/com_users');
$lang->load('', JPATH_ROOT);
/* ----------------------- /* -----------------------
Load assets via WebAssetManager (matches index.php pattern) Load ONLY template.css + theme palettes (with min toggle)
------------------------ */ ------------------------ */
$params_developmentmode = (bool) $params->get('developmentmode', false) || (bool) $app->get('debug', false); $useMin = !((int) $params->get('development_mode', 0) === 1);
$suffix = $params_developmentmode ? '' : '.min'; $assetSuffix = $useMin ? '.min' : '';
$base = rtrim(Uri::root(true), '/') . '/templates/' . $this->template . '/css/';
$jsBase = rtrim(Uri::root(true), '/') . '/templates/' . $this->template . '/js/';
// Core template CSS + offline overlay CSS $doc->addStyleSheet($base . 'template' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-template']);
$wa->useStyle('template.base' . $suffix);
$wa->useStyle('template.offline' . $suffix);
// Osaka font /* Load theme palettes */
$wa->useStyle('template.font.osaka'); $doc->addStyleSheet($base . 'theme/light.standard' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-light-standard']);
$doc->addStyleSheet($base . 'theme/dark.standard' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-dark-standard']);
// Font Awesome 7 Free /* Load custom palettes only if selected in template configuration AND files exist */
$wa->useStyle('vendor.fa7free.all' . $suffix);
// Theme palettes
$wa->useStyle('template.light.standard' . $suffix);
$wa->useStyle('template.dark.standard' . $suffix);
// Custom palettes (if selected and files exist)
$params_LightColorName = (string) $params->get('colorLightName', 'standard'); $params_LightColorName = (string) $params->get('colorLightName', 'standard');
$params_DarkColorName = (string) $params->get('colorDarkName', 'standard'); $params_DarkColorName = (string) $params->get('colorDarkName', 'standard');
if ($params_LightColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokocassiopeia/css/theme/light.custom.css')) if ($params_LightColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokocassiopeia/css/theme/light.custom.css'))
{ {
$wa->useStyle('template.light.custom' . $suffix); $doc->addStyleSheet($base . 'theme/light.custom' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-light-custom']);
} }
if ($params_DarkColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokocassiopeia/css/theme/dark.custom.css')) if ($params_DarkColorName === 'custom' && file_exists(JPATH_ROOT . '/media/templates/site/mokocassiopeia/css/theme/dark.custom.css'))
{ {
$wa->useStyle('template.dark.custom' . $suffix); $doc->addStyleSheet($base . 'theme/dark.custom' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-dark-custom']);
} }
// User overrides (loaded last) /* Load user assets last (after all other styles and scripts) */
$wa->useStyle('template.user'); $doc->addStyleSheet($base . 'user' . $assetSuffix . '.css', ['version' => 'auto'], ['id' => 'moko-user']);
// Accessibility high-contrast stylesheet /* Bootstrap CSS/JS for accordion behavior; safe to keep. */
$wa->useStyle('template.a11y-high-contrast'); HTMLHelper::_('bootstrap.loadCss', true, $doc);
// Template JS (theme switcher, a11y toolbar, var-copy, etc.)
if ($params_developmentmode) {
$wa->useScript('template.js');
} else {
$wa->useScript('template.js.min');
}
$wa->useScript('user.js');
// Bootstrap CSS + JS (accordion, responsive grid, utilities)
try {
$wa->useStyle('bootstrap.css');
} catch (\Exception $e) {
// Fallback: load via HTMLHelper
HTMLHelper::_('bootstrap.loadCss', true, $doc);
}
HTMLHelper::_('bootstrap.framework'); HTMLHelper::_('bootstrap.framework');
/* Load template.js for theme switcher and other functionality */
$doc->addScript($jsBase . 'template' . $assetSuffix . '.js', ['version' => 'auto', 'defer' => true], ['id' => 'moko-template-js']);
/* Load user.js last for custom user scripts */
$doc->addScript($jsBase . 'user' . $assetSuffix . '.js', ['version' => 'auto', 'defer' => true], ['id' => 'moko-user-js']);
/* ----------------------- /* -----------------------
Title + Meta Title + Meta (Include Site Name in Page Titles)
------------------------ */ ------------------------ */
$sitename = (string) $app->get('sitename'); $sitename = (string) $app->get('sitename');
$baseTitle = Text::_('JGLOBAL_OFFLINE') ?: 'Offline'; $baseTitle = Text::_('JGLOBAL_OFFLINE') ?: 'Offline';
$snSetting = (int) $app->get('sitename_pagetitles', 0); $snSetting = (int) $app->get('sitename_pagetitles', 0); // 0=no, 1=before, 2=after
if ($snSetting === 1) { if ($snSetting === 1) {
$doc->setTitle(Text::sprintf('JPAGETITLE', $sitename, $baseTitle)); $doc->setTitle(Text::sprintf('JPAGETITLE', $sitename, $baseTitle)); // Site Name BEFORE
} elseif ($snSetting === 2) { } elseif ($snSetting === 2) {
$doc->setTitle(Text::sprintf('JPAGETITLE', $baseTitle, $sitename)); $doc->setTitle(Text::sprintf('JPAGETITLE', $baseTitle, $sitename)); // Site Name AFTER
} else { } else {
$doc->setTitle($baseTitle); $doc->setTitle($baseTitle);
} }
@@ -118,21 +87,11 @@ $doc->setMetaData('robots', 'noindex, nofollow');
/* ----------------------- /* -----------------------
Offline content from Global Config Offline content from Global Config
------------------------ */ ------------------------ */
$displayOfflineMessage = (int) $app->get('display_offline_message', 1); $displayOfflineMessage = (int) $app->get('display_offline_message', 1); // 0|1|2
$offlineMessage = trim((string) $app->get('offline_message', '')); $offlineMessage = trim((string) $app->get('offline_message', ''));
/* ----------------------- /* -----------------------
Offline image from Joomla Global Config (System > Global Configuration > Site > Offline Image) Brand: logo from params OR siteTitle (matches index.php)
Used as the full-viewport background image.
------------------------ */
$offlineImage = trim((string) $app->get('offline_image', ''));
$bgStyle = '';
if ($offlineImage !== '') {
$bgStyle = 'background-image: url(\'' . htmlspecialchars(Uri::root(false) . $offlineImage, ENT_QUOTES, 'UTF-8') . '\');';
}
/* -----------------------
Brand: logo from template params OR siteTitle
------------------------ */ ------------------------ */
$brandHtml = ''; $brandHtml = '';
$logoFile = (string) $params->get('logoFile'); $logoFile = (string) $params->get('logoFile');
@@ -147,8 +106,9 @@ if ($logoFile !== '') {
0 0
); );
} else { } else {
// If no logo file, show the title (defaults to "MokoCassiopeia" if not set)
$siteTitle = $params->get('siteTitle', 'MokoCassiopeia'); $siteTitle = $params->get('siteTitle', 'MokoCassiopeia');
$brandHtml = '<span class="site-title" title="' . htmlspecialchars($sitename, ENT_QUOTES, 'UTF-8') . '">' $brandHtml = '<span class="site-title" title="' . $sitename . '">'
. htmlspecialchars($siteTitle, ENT_COMPAT, 'UTF-8') . htmlspecialchars($siteTitle, ENT_COMPAT, 'UTF-8')
. '</span>'; . '</span>';
} }
@@ -156,34 +116,8 @@ if ($logoFile !== '') {
$brandTagline = (string) ($params->get('brand_tagline') ?: $params->get('siteDescription') ?: ''); $brandTagline = (string) ($params->get('brand_tagline') ?: $params->get('siteDescription') ?: '');
$showTagline = (int) $params->get('show_brand_tagline', 0); $showTagline = (int) $params->get('show_brand_tagline', 0);
// Favicon
$params_favicon_source = (string) $params->get('favicon_source', '');
$faviconHeadTags = '';
if ($params_favicon_source) {
require_once JPATH_ROOT . '/templates/' . $this->template . '/helper/favicon.php';
$faviconSourceAbs = JPATH_ROOT . '/' . ltrim($params_favicon_source, '/');
$faviconOutputDir = JPATH_ROOT . '/images/favicons';
$faviconUrlBase = Uri::root(true) . '/images/favicons';
if (MokoFaviconHelper::generate($faviconSourceAbs, $faviconOutputDir)) {
$faviconHeadTags = MokoFaviconHelper::getHeadTags($faviconUrlBase);
}
}
// Theme params // Theme params
$params_theme_enabled = (int) $params->get('theme_enabled', 1); $params_theme_enabled = (int) $params->get('theme_enabled', 1);
$params_theme_fab_enabled = (int) $params->get('theme_fab_enabled', 1);
$params_theme_fab_pos = 'br';
// Accessibility params
$params_a11y_toolbar = (int) $params->get('a11y_toolbar_enabled', 1);
$params_a11y_resize = (int) $params->get('a11y_text_resize', 1);
$params_a11y_invert = (int) $params->get('a11y_color_inversion', 1);
$params_a11y_contrast = (int) $params->get('a11y_high_contrast', 1);
$params_a11y_links = (int) $params->get('a11y_highlight_links', 1);
$params_a11y_font = (int) $params->get('a11y_readable_font', 1);
$params_a11y_animations = (int) $params->get('a11y_pause_animations', 1);
$params_a11y_pos = 'br';
// Analytics params // Analytics params
$params_googletagmanager = $params->get('googletagmanager', false); $params_googletagmanager = $params->get('googletagmanager', false);
@@ -197,7 +131,7 @@ if (!empty($params_googlesitekey)) {
} }
/* ----------------------- /* -----------------------
Login routes Login routes & Users
------------------------ */ ------------------------ */
$action = Route::_('index.php', true); $action = Route::_('index.php', true);
$return = base64_encode(Uri::base()); $return = base64_encode(Uri::base());
@@ -218,12 +152,10 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
<head> <head>
<jdoc:include type="head" /> <jdoc:include type="head" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<?php if ($faviconHeadTags) : ?>
<?php echo $faviconHeadTags; ?>
<?php endif; ?>
<?php if ($params_theme_enabled) : ?> <?php if ($params_theme_enabled) : ?>
<script> <script>
// Early theme application to avoid FOUC
(function () { (function () {
try { try {
var stored = localStorage.getItem('theme'); var stored = localStorage.getItem('theme');
@@ -236,21 +168,20 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
</script> </script>
<?php endif; ?> <?php endif; ?>
<style>
.moko-offline-wrap { min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; }
.moko-offline-main { display: grid; place-items: center; padding: 2rem 1rem; }
.moko-card { max-width: 720px; width: 100%; }
.moko-brand { display:flex; align-items:center; gap:.75rem; text-decoration:none; }
.moko-brand .brand-tagline { display:block; opacity:.75; font-size:.875rem; line-height:1.2; }
.skip-link { position:absolute; left:-9999px; top:auto; width:1px; height:1px; overflow:hidden; }
.skip-link:focus { position:static; width:auto; height:auto; padding:.5rem 1rem; }
</style>
</head> </head>
<body class="site moko-offline-wrap" <body class="site moko-offline-wrap <?php echo htmlspecialchars($direction, ENT_QUOTES, 'UTF-8'); ?>">
data-theme-fab-enabled="<?php echo $params_theme_fab_enabled ? '1' : '0'; ?>"
data-theme-fab-pos="<?php echo htmlspecialchars($params_theme_fab_pos, ENT_QUOTES, 'UTF-8'); ?>"
data-a11y-toolbar="<?php echo $params_a11y_toolbar ? '1' : '0'; ?>"
data-a11y-resize="<?php echo $params_a11y_resize ? '1' : '0'; ?>"
data-a11y-invert="<?php echo $params_a11y_invert ? '1' : '0'; ?>"
data-a11y-contrast="<?php echo $params_a11y_contrast ? '1' : '0'; ?>"
data-a11y-links="<?php echo $params_a11y_links ? '1' : '0'; ?>"
data-a11y-font="<?php echo $params_a11y_font ? '1' : '0'; ?>"
data-a11y-animations="<?php echo $params_a11y_animations ? '1' : '0'; ?>"
data-a11y-pos="<?php echo htmlspecialchars($params_a11y_pos, ENT_QUOTES, 'UTF-8'); ?>"
<?php if ($bgStyle) : ?>style="<?php echo $bgStyle; ?>"<?php endif; ?>>
<?php if (!empty($params_googletagmanager) && !empty($params_googletagmanagerid)) : <?php if (!empty($params_googletagmanager) && !empty($params_googletagmanagerid)) :
$gtmID = htmlspecialchars($params_googletagmanagerid, ENT_QUOTES, 'UTF-8'); ?> $gtmID = htmlspecialchars($params_googletagmanagerid, ENT_QUOTES, 'UTF-8'); ?>
<!-- Google Tag Manager -->
<script> <script>
(function(w,d,s,l,i){ (function(w,d,s,l,i){
w[l]=w[l]||[]; w[l]=w[l]||[];
@@ -263,14 +194,19 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
f.parentNode.insertBefore(j,f); f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','<?php echo $gtmID; ?>'); })(window,document,'script','dataLayer','<?php echo $gtmID; ?>');
</script> </script>
<!-- End Google Tag Manager -->
<!-- Google Tag Manager (noscript) -->
<noscript> <noscript>
<iframe src="https://www.googletagmanager.com/ns.html?id=<?php echo $gtmID; ?>" <iframe src="https://www.googletagmanager.com/ns.html?id=<?php echo $gtmID; ?>"
height="0" width="0" style="display:none;visibility:hidden"></iframe> height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript> </noscript>
<!-- End Google Tag Manager (noscript) -->
<?php endif; ?> <?php endif; ?>
<?php if (!empty($params_googleanalytics) && !empty($params_googleanalyticsid)) : <?php if (!empty($params_googleanalytics) && !empty($params_googleanalyticsid)) :
$gaId = htmlspecialchars($params_googleanalyticsid, ENT_QUOTES, 'UTF-8'); ?> $gaId = htmlspecialchars($params_googleanalyticsid, ENT_QUOTES, 'UTF-8'); ?>
<!-- Google Analytics (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<?php echo $gaId; ?>"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=<?php echo $gaId; ?>"></script>
<script> <script>
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
@@ -287,46 +223,65 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
gtag('config', id, { 'anonymize_ip': true }); gtag('config', id, { 'anonymize_ip': true });
} else if (/^UA-/.test(id)) { } else if (/^UA-/.test(id)) {
gtag('config', id, { 'anonymize_ip': true }); gtag('config', id, { 'anonymize_ip': true });
console.warn('Using a UA- ID. Universal Analytics is sunset; consider migrating to GA4.');
} else { } else {
console.warn('Unrecognized Google Analytics ID format:', id); console.warn('Unrecognized Google Analytics ID format:', id);
} }
})('<?php echo $gaId; ?>'); })('<?php echo $gaId; ?>');
</script> </script>
<!-- End Google Analytics -->
<?php endif; ?> <?php endif; ?>
<a class="skip-link" href="#maincontent"><?php echo Text::_('JSKIP_TO_CONTENT') ?: 'Skip to content'; ?></a> <a class="skip-link" href="#maincontent"><?php echo Text::_('JSKIP_TO_CONTENT') ?: 'Skip to content'; ?></a>
<!-- Centered overlay card --> <header class="container-header header py-3">
<main id="maincontent"> <div class="grid-child container-nav d-flex align-items-center gap-3">
<div class="moko-offline-card">
<!-- Logo --> <!-- Brand (mutually exclusive image/text) -->
<a class="moko-offline-brand" href="<?php echo htmlspecialchars(Uri::base(), ENT_QUOTES, 'UTF-8'); ?>" aria-label="<?php echo htmlspecialchars($sitename, ENT_COMPAT, 'UTF-8'); ?>"> <a class="moko-brand me-auto" href="<?php echo htmlspecialchars(Uri::base(), ENT_QUOTES, 'UTF-8'); ?>" aria-label="<?php echo htmlspecialchars($sitename, ENT_COMPAT, 'UTF-8'); ?>">
<?php echo $brandHtml; ?> <?php echo $brandHtml; ?>
<?php if ($showTagline && $brandTagline): ?> <?php if ($showTagline && $brandTagline): ?>
<small class="brand-tagline"><?php echo htmlspecialchars($brandTagline, ENT_COMPAT, 'UTF-8'); ?></small> <small class="brand-tagline"><?php echo htmlspecialchars($brandTagline, ENT_COMPAT, 'UTF-8'); ?></small>
<?php endif; ?> <?php endif; ?>
</a> </a>
<!-- Offline message: 0=hidden, 1=custom message, 2=system language string --> <!-- Header module position: offline-header -->
<?php if ($this->countModules('offline-header')) : ?>
<div class="ms-2">
<jdoc:include type="modules" name="offline-header" style="none" />
</div>
<?php endif; ?>
</div>
</header>
<main id="maincontent" class="moko-offline-main">
<div class="container">
<jdoc:include type="message" />
<div class="moko-card card shadow-sm rounded-3 p-4 p-md-5">
<?php if ($displayOfflineMessage === 1 && $offlineMessage !== '') : ?> <?php if ($displayOfflineMessage === 1 && $offlineMessage !== '') : ?>
<div class="moko-offline-message"> <div class="mb-4">
<p><?php echo $offlineMessage; ?></p> <h1 class="h3 mb-2"><?php echo Text::_('JOFFLINE_MESSAGE') ?: 'Site Offline'; ?></h1>
<p class="lead mb-0"><?php echo $offlineMessage; ?></p>
</div> </div>
<?php elseif ($displayOfflineMessage === 2) : ?> <?php elseif ($displayOfflineMessage === 2) : ?>
<div class="moko-offline-message"> <div class="mb-4">
<p><?php echo Text::_('JOFFLINE_MESSAGE') ?: 'This site is down for maintenance.'; ?></p> <h1 class="h3 mb-2"><?php echo Text::_('JOFFLINE_MESSAGE') ?: 'Site Offline'; ?></h1>
<p class="lead mb-0">
<?php echo Text::_('JOFFLINE_MESSAGE_DEFAULT') ?: 'This site is down for maintenance. Please check back soon.'; ?>
</p>
</div> </div>
<?php endif; ?> <?php endif; ?>
<!-- Offline module position --> <!-- Main offline module position -->
<?php if ($this->countModules('offline')) : ?> <?php if ($this->countModules('offline')) : ?>
<div class="moko-offline-modules"> <section class="mb-4" aria-label="Offline modules">
<jdoc:include type="modules" name="offline" style="none" /> <jdoc:include type="modules" name="offline" style="none" />
</div> </section>
<?php endif; ?> <?php endif; ?>
<!-- Login accordion --> <!-- Login UNDER an accordion (collapsed by default) -->
<div class="accordion" id="offlineAccordion"> <div class="accordion" id="offlineAccordion">
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header" id="headingLogin"> <h2 class="accordion-header" id="headingLogin">
@@ -392,23 +347,12 @@ if (class_exists('\Joomla\Component\Users\Site\Helper\RouteHelper')) {
</div> </div>
</div> </div>
</div> </div>
<!-- /accordion -->
<!-- Copyright -->
<div class="moko-offline-copyright">
<div>&copy; <?php echo date('Y'); ?> <?php echo htmlspecialchars($sitename, ENT_COMPAT, 'UTF-8'); ?></div>
<div><?php echo Text::_('MOD_FOOTER_LINE2'); ?></div>
</div> </div>
</div> </div>
</main> </main>
<!-- Offline footer module position --> <!-- No footer modules on offline page -->
<?php if ($this->countModules('offline-footer')) : ?>
<div class="moko-offline-messages mt-3">
<jdoc:include type="modules" name="offline-footer" style="none" />
</div>
<?php endif; ?>
<jdoc:include type="modules" name="debug" style="none" /> <jdoc:include type="modules" name="debug" style="none" />
</body> </body>
</html> </html>

View File

@@ -1,6 +1,6 @@
<?php <?php
/** /**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* *
* This file is part of a Moko Consulting project. * This file is part of a Moko Consulting project.
* *
@@ -8,40 +8,49 @@
*/ */
/** /**
* MokoCassiopeia install/update/uninstall script. * Template install/update/uninstall script.
* * Joomla calls the methods in this class automatically during template
* On update: copies the template as MokoOnyx (new directory), updates the * install, update, and uninstall via the <scriptfile> element in
* database to register MokoOnyx, migrates styles + params, and sets it as * templateDetails.xml.
* the default site template. The old MokoCassiopeia directory stays intact * Joomla 5 and 6 compatible — uses the InstallerScriptInterface when
* (Joomla's installer still needs it) — the user can uninstall it later. * available, falls back to the legacy class-based approach otherwise.
*/ */
defined('_JEXEC') or die; defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Installer\InstallerAdapter; use Joomla\CMS\Installer\InstallerAdapter;
use Joomla\CMS\Installer\InstallerScriptInterface;
use Joomla\CMS\Log\Log; use Joomla\CMS\Log\Log;
class Tpl_MokocassiopeiaInstallerScript implements InstallerScriptInterface class Tpl_MokocassiopeiaInstallerScript
{ {
/**
* Minimum PHP version required by this template.
*/
private const MIN_PHP = '8.1.0'; private const MIN_PHP = '8.1.0';
/**
* Minimum Joomla version required by this template.
*/
private const MIN_JOOMLA = '4.4.0'; private const MIN_JOOMLA = '4.4.0';
private const OLD_NAME = 'mokocassiopeia'; /**
private const NEW_NAME = 'mokoonyx'; * Called before install/update/uninstall.
private const OLD_DISPLAY = 'MokoCassiopeia'; *
private const NEW_DISPLAY = 'MokoOnyx'; * @param string $type install, update, discover_install, or uninstall.
* @param InstallerAdapter $parent The adapter calling this method.
private const ONYX_UPDATES_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml'; *
* @return bool True to proceed, false to abort.
// ── Joomla lifecycle ─────────────────────────────────────────────── */
public function preflight(string $type, InstallerAdapter $parent): bool public function preflight(string $type, InstallerAdapter $parent): bool
{ {
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) { if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
Factory::getApplication()->enqueueMessage( Factory::getApplication()->enqueueMessage(
sprintf('MokoCassiopeia requires PHP %s+. Running %s.', self::MIN_PHP, PHP_VERSION), sprintf(
'MokoCassiopeia requires PHP %s or later. You are running PHP %s.',
self::MIN_PHP,
PHP_VERSION
),
'error' 'error'
); );
return false; return false;
@@ -49,7 +58,11 @@ class Tpl_MokocassiopeiaInstallerScript implements InstallerScriptInterface
if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) { if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) {
Factory::getApplication()->enqueueMessage( Factory::getApplication()->enqueueMessage(
sprintf('MokoCassiopeia requires Joomla %s+. Running %s.', self::MIN_JOOMLA, JVERSION), sprintf(
'MokoCassiopeia requires Joomla %s or later. You are running Joomla %s.',
self::MIN_JOOMLA,
JVERSION
),
'error' 'error'
); );
return false; return false;
@@ -58,364 +71,151 @@ class Tpl_MokocassiopeiaInstallerScript implements InstallerScriptInterface
return true; return true;
} }
/**
* Called after a successful install.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function install(InstallerAdapter $parent): bool public function install(InstallerAdapter $parent): bool
{ {
$this->log('MokoCassiopeia installed.'); $this->logMessage('MokoCassiopeia template installed.');
return true; return true;
} }
/**
* Called after a successful update.
*
* This is where the CSS variable sync runs — it detects variables that
* were added in the new version and injects them into the user's custom
* palette files without overwriting existing values.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function update(InstallerAdapter $parent): bool public function update(InstallerAdapter $parent): bool
{ {
$this->log('MokoCassiopeia update() — version ' . ($parent->getManifest()->version ?? '?')); $this->logMessage('MokoCassiopeia template updated.');
// Run CSS variable sync to inject any new variables into user's custom palettes.
$synced = $this->syncCustomVariables($parent);
if ($synced > 0) {
Factory::getApplication()->enqueueMessage(
sprintf(
'MokoCassiopeia: %d new CSS variable(s) were added to your custom palette files. '
. 'Review them in your light.custom.css and/or dark.custom.css to customise the new defaults.',
$synced
),
'notice'
);
}
return true; return true;
} }
/**
* Called after a successful uninstall.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function uninstall(InstallerAdapter $parent): bool public function uninstall(InstallerAdapter $parent): bool
{ {
$this->log('MokoCassiopeia uninstalled.'); $this->logMessage('MokoCassiopeia template uninstalled.');
return true; return true;
} }
/**
* Called after install/update completes (regardless of type).
*
* @param string $type install, update, or discover_install.
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return bool
*/
public function postflight(string $type, InstallerAdapter $parent): bool public function postflight(string $type, InstallerAdapter $parent): bool
{ {
// Debug: write directly to confirm script runs
@file_put_contents(
JPATH_ROOT . '/administrator/logs/bridge_debug.txt',
date('Y-m-d H:i:s') . " postflight called, type={$type}\n",
FILE_APPEND
);
if ($type === 'update') {
$this->log('=== MokoCassiopeia → MokoOnyx bridge ===');
$this->bridge();
}
return true; return true;
} }
// ── Bridge ───────────────────────────────────────────────────────── /**
* Run the CSS variable sync utility.
private function bridge(): void *
* Loads sync_custom_vars.php from the template directory and calls
* MokoCssVarSync::run() to detect and inject missing variables.
*
* @param InstallerAdapter $parent The adapter calling this method.
*
* @return int Number of variables added across all files.
*/
private function syncCustomVariables(InstallerAdapter $parent): int
{ {
$app = Factory::getApplication(); $templateDir = $parent->getParent()->getPath('source');
// 1. Copy template directory (don't rename — Joomla still needs the old one) // The sync script lives alongside this script in the template root.
$copied = $this->copyTemplateDir(); $syncScript = $templateDir . '/sync_custom_vars.php';
if (!$copied && !is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) {
$app->enqueueMessage( if (!is_file($syncScript)) {
'MokoOnyx bridge: could not create template directory. ' $this->logMessage('CSS variable sync script not found at: ' . $syncScript, 'warning');
. 'Please copy <code>templates/mokocassiopeia</code> to <code>templates/mokoonyx</code> manually.', return 0;
'warning' }
require_once $syncScript;
if (!class_exists('MokoCssVarSync')) {
$this->logMessage('MokoCssVarSync class not found after loading script.', 'warning');
return 0;
}
try {
$joomlaRoot = JPATH_ROOT;
$results = MokoCssVarSync::run($joomlaRoot);
$totalAdded = 0;
foreach ($results as $filePath => $result) {
$totalAdded += count($result['added']);
if (!empty($result['added'])) {
$this->logMessage(
sprintf(
'CSS sync: added %d variable(s) to %s',
count($result['added']),
basename($filePath)
)
); );
return;
}
// 2. Copy media directory
$this->copyMediaDir();
// 3. Register MokoOnyx in #__extensions (if not already there)
$this->registerExtension();
// 4. Migrate template styles (create MokoOnyx styles with same params)
$this->migrateStyles();
// 5. Redirect update server to MokoOnyx
$this->updateUpdateServer();
// 6. Unlock MokoCassiopeia (allow uninstall) + lock MokoOnyx (prevent accidental uninstall)
$this->updateExtensionLocks();
// 7. Notify
$app->enqueueMessage(
'<strong>MokoOnyx has been installed as a replacement for MokoCassiopeia.</strong><br>'
. 'Your template settings have been migrated. MokoOnyx is now your active site template.<br>'
. 'MokoCassiopeia has been unlocked — you can uninstall it from Extensions &rarr; Manage.',
'success'
);
$this->log('=== Bridge completed ===');
}
// ── Copy directories ───────────────────────────────────────────────
private function copyTemplateDir(): bool
{
$src = JPATH_ROOT . '/templates/' . self::OLD_NAME;
$dst = JPATH_ROOT . '/templates/' . self::NEW_NAME;
if (is_dir($dst)) {
$this->log('Bridge: templates/' . self::NEW_NAME . ' already exists — skipping copy.');
return true;
}
if (!is_dir($src)) {
$this->log('Bridge: source template dir not found.', 'error');
return false;
}
$result = $this->recursiveCopy($src, $dst);
$this->log('Bridge: ' . ($result ? 'copied' : 'FAILED to copy') . ' template dir → ' . self::NEW_NAME);
return $result;
}
private function copyMediaDir(): void
{
$src = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME;
$dst = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME;
if (is_dir($dst)) {
$this->log('Bridge: media dir already exists — skipping.');
return;
}
if (!is_dir($src)) {
$this->log('Bridge: source media dir not found — skipping.');
return;
}
$result = $this->recursiveCopy($src, $dst);
$this->log('Bridge: ' . ($result ? 'copied' : 'FAILED to copy') . ' media dir → ' . self::NEW_NAME);
}
private function recursiveCopy(string $src, string $dst): bool
{
if (!mkdir($dst, 0755, true) && !is_dir($dst)) {
return false;
}
$dir = opendir($src);
if ($dir === false) {
return false;
}
while (($file = readdir($dir)) !== false) {
if ($file === '.' || $file === '..') {
continue;
}
$srcPath = $src . '/' . $file;
$dstPath = $dst . '/' . $file;
if (is_dir($srcPath)) {
$this->recursiveCopy($srcPath, $dstPath);
} else {
copy($srcPath, $dstPath);
} }
} }
closedir($dir); return $totalAdded;
return true;
}
// ── Database updates ───────────────────────────────────────────────
private function registerExtension(): void
{
$db = Factory::getDbo();
// Check if MokoOnyx is already registered
$query = $db->getQuery(true)
->select('extension_id')
->from('#__extensions')
->where($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME))
->where($db->quoteName('type') . ' = ' . $db->quote('template'));
$exists = (int) $db->setQuery($query)->loadResult();
if ($exists) {
$this->log('Bridge: MokoOnyx already registered in #__extensions (id=' . $exists . ').');
return;
}
// Copy the MokoCassiopeia extension row and change element/name
$query = $db->getQuery(true)
->select('*')
->from('#__extensions')
->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
->where($db->quoteName('type') . ' = ' . $db->quote('template'));
$oldExt = $db->setQuery($query)->loadObject();
if (!$oldExt) {
$this->log('Bridge: MokoCassiopeia not found in #__extensions.', 'warning');
return;
}
$newExt = clone $oldExt;
unset($newExt->extension_id);
$newExt->element = self::NEW_NAME;
$newExt->name = self::NEW_NAME;
// Update manifest_cache to reflect new name
if (is_string($newExt->manifest_cache)) {
$newExt->manifest_cache = str_replace(self::OLD_NAME, self::NEW_NAME, $newExt->manifest_cache);
$newExt->manifest_cache = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $newExt->manifest_cache);
}
try {
$db->insertObject('#__extensions', $newExt, 'extension_id');
$this->log('Bridge: registered MokoOnyx in #__extensions (id=' . $newExt->extension_id . ').');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->log('Bridge: failed to register extension: ' . $e->getMessage(), 'error'); $this->logMessage('CSS variable sync failed: ' . $e->getMessage(), 'error');
return 0;
} }
} }
private function migrateStyles(): void /**
* Log a message to Joomla's log system.
*
* @param string $message The log message.
* @param string $priority Log priority (info, warning, error).
*/
private function logMessage(string $message, string $priority = 'info'): void
{ {
$db = Factory::getDbo(); $priorities = [
'info' => Log::INFO,
'warning' => Log::WARNING,
'error' => Log::ERROR,
];
// Get all MokoCassiopeia styles
$query = $db->getQuery(true)
->select('*')
->from('#__template_styles')
->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME))
->where($db->quoteName('client_id') . ' = 0');
$oldStyles = $db->setQuery($query)->loadObjectList();
if (empty($oldStyles)) {
$this->log('Bridge: no MokoCassiopeia styles found.');
return;
}
$this->log('Bridge: migrating ' . count($oldStyles) . ' style(s).');
foreach ($oldStyles as $old) {
$newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $old->title);
$newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle);
// Skip if MokoOnyx already has this style
$check = $db->getQuery(true)
->select('COUNT(*)')
->from('#__template_styles')
->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME))
->where($db->quoteName('title') . ' = ' . $db->quote($newTitle));
if ((int) $db->setQuery($check)->loadResult() > 0) {
$this->log("Bridge: style '{$newTitle}' already exists — skipping.");
continue;
}
$newParams = is_string($old->params)
? str_replace(self::OLD_NAME, self::NEW_NAME, $old->params)
: $old->params;
$new = clone $old;
unset($new->id);
$new->template = self::NEW_NAME;
$new->title = $newTitle;
$new->params = $newParams;
$new->home = 0;
try {
$db->insertObject('#__template_styles', $new, 'id');
$newId = $new->id;
// If old was default, make new default
if ($old->home == 1) {
$db->setQuery(
$db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('home') . ' = 1')
->where('id = ' . (int) $newId)
)->execute();
$db->setQuery(
$db->getQuery(true)
->update('#__template_styles')
->set($db->quoteName('home') . ' = 0')
->where('id = ' . (int) $old->id)
)->execute();
$this->log('Bridge: set MokoOnyx as default site template.');
}
$this->log("Bridge: created style '{$newTitle}'.");
} catch (\Throwable $e) {
$this->log("Bridge: failed to create style '{$newTitle}': " . $e->getMessage(), 'warning');
}
}
}
private function updateUpdateServer(): void
{
$db = Factory::getDbo();
try {
$query = $db->getQuery(true)
->update('#__update_sites')
->set($db->quoteName('location') . ' = ' . $db->quote(self::ONYX_UPDATES_URL))
->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_DISPLAY))
->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
$db->setQuery($query)->execute();
$n = $db->getAffectedRows();
if ($n > 0) {
$this->log("Bridge: redirected {$n} update site(s) to MokoOnyx.");
}
} catch (\Throwable $e) {
$this->log('Bridge: update server redirect failed: ' . $e->getMessage(), 'warning');
}
// Clear cached updates
try {
$db->setQuery(
$db->getQuery(true)
->delete('#__updates')
->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
)->execute();
} catch (\Throwable $e) {
// Not critical
}
}
private function updateExtensionLocks(): void
{
$db = Factory::getDbo();
// Unlock MokoCassiopeia — allow uninstall
try {
$query = $db->getQuery(true)
->update('#__extensions')
->set($db->quoteName('locked') . ' = 0')
->set($db->quoteName('protected') . ' = 0')
->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
->where($db->quoteName('type') . ' = ' . $db->quote('template'));
$db->setQuery($query)->execute();
if ($db->getAffectedRows() > 0) {
$this->log('Bridge: unlocked MokoCassiopeia (can be uninstalled).');
}
} catch (\Throwable $e) {
$this->log('Bridge: failed to unlock MokoCassiopeia: ' . $e->getMessage(), 'warning');
}
// Lock MokoOnyx — prevent accidental uninstall
try {
$query = $db->getQuery(true)
->update('#__extensions')
->set($db->quoteName('locked') . ' = 1')
->where($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME))
->where($db->quoteName('type') . ' = ' . $db->quote('template'));
$db->setQuery($query)->execute();
if ($db->getAffectedRows() > 0) {
$this->log('Bridge: locked MokoOnyx (protected from uninstall).');
}
} catch (\Throwable $e) {
$this->log('Bridge: failed to lock MokoOnyx: ' . $e->getMessage(), 'warning');
}
}
// ── Logging ────────────────────────────────────────────────────────
private function log(string $message, string $priority = 'info'): void
{
static $init = false;
if (!$init) {
Log::addLogger( Log::addLogger(
['text_file' => 'mokocassiopeia_bridge.log.php'], ['text_file' => 'mokocassiopeia.log.php'],
Log::ALL, Log::ALL,
['mokocassiopeia_bridge'] ['mokocassiopeia']
); );
$init = true;
}
$levels = ['info' => Log::INFO, 'warning' => Log::WARNING, 'error' => Log::ERROR]; Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokocassiopeia');
Log::add($message, $levels[$priority] ?? Log::INFO, 'mokocassiopeia_bridge');
} }
} }

View File

@@ -34,11 +34,11 @@ final class MokoCssVarSync
*/ */
private const PALETTES = [ private const PALETTES = [
[ [
'starter' => 'media/css/theme/light.standard.css', 'starter' => 'templates/light.custom.css',
'user' => 'media/templates/site/%s/css/theme/light.custom.css', 'user' => 'media/templates/site/%s/css/theme/light.custom.css',
], ],
[ [
'starter' => 'media/css/theme/dark.standard.css', 'starter' => 'templates/dark.custom.css',
'user' => 'media/templates/site/%s/css/theme/dark.custom.css', 'user' => 'media/templates/site/%s/css/theme/dark.custom.css',
], ],
]; ];
@@ -97,24 +97,28 @@ final class MokoCssVarSync
private static function syncFile(string $starterPath, string $userPath): array private static function syncFile(string $starterPath, string $userPath): array
{ {
$starterVars = self::extractVarsWithContext($starterPath); $starterVars = self::extractVarsWithContext($starterPath);
$userVarsMap = self::extractVarsWithContext($userPath); $userVars = self::extractVarNames($userPath);
$userNames = self::extractVarNames($userPath);
// Find missing variables
$missing = []; $missing = [];
foreach ($starterVars as $name => $declaration) { foreach ($starterVars as $name => $declaration) {
if (!isset($userNames[$name])) { if (!isset($userVars[$name])) {
$missing[$name] = $declaration; $missing[$name] = $declaration;
} }
} }
// Rebuild the entire :root block in starter file order. if (empty($missing)) {
// User's custom values are preserved; missing vars get starter defaults. return ['added' => [], 'skipped' => []];
$reordered = self::rebuildInStarterOrder($starterPath, $userVarsMap, $missing); }
// Replace the :root block in the user file with the reordered version. // Group missing variables by their section comment header.
$sections = self::groupBySection($missing, $starterPath);
// Build the injection block.
$injection = self::buildInjectionBlock($sections);
// Insert before the closing } of the :root rule.
$userCss = file_get_contents($userPath); $userCss = file_get_contents($userPath);
$userCss = self::replaceRootBlock($userCss, $reordered); $userCss = self::injectBeforeRootClose($userCss, $injection);
// Write back (atomic: write to .tmp then rename). // Write back (atomic: write to .tmp then rename).
$tmpPath = $userPath . '.tmp'; $tmpPath = $userPath . '.tmp';
@@ -124,104 +128,6 @@ final class MokoCssVarSync
return ['added' => array_keys($missing), 'skipped' => []]; return ['added' => array_keys($missing), 'skipped' => []];
} }
/**
* Rebuild all variables in the order they appear in the starter file.
* User values are preserved; missing vars use starter defaults.
*
* @param string $starterPath Path to starter file.
* @param array $userVars User's variable name => declaration.
* @param array $missing Missing variable name => starter declaration.
* @return string Complete CSS content for inside :root { }.
*/
private static function rebuildInStarterOrder(string $starterPath, array $userVars, array $missing): string
{
$lines = file($starterPath, FILE_IGNORE_NEW_LINES);
$output = [];
$inRoot = false;
$depth = 0;
foreach ($lines as $line) {
// Track when we enter :root (brace may be on same line)
if (!$inRoot && preg_match('/:root/', $line)) {
$inRoot = true;
// If { is on this same line, don't skip it — just continue processing
if (strpos($line, '{') === false) {
continue;
}
// Fall through to process the rest of this line
}
if (!$inRoot) {
continue;
}
// Track braces (skip lines that are ONLY a brace)
$trimmed = trim($line);
if ($trimmed === '{') {
continue;
}
if ($trimmed === '}') {
break; // End of :root
}
// Section comment headers — always include
if (preg_match('/\/\*\s*=+\s*.+?\s*=+\s*\*\//', $line)) {
$output[] = $line;
continue;
}
// Regular comments — include
if (preg_match('/^\s*\/\*/', $line) || preg_match('/^\s*\*/', $line)) {
$output[] = $line;
continue;
}
// Blank lines — include
if (trim($line) === '') {
$output[] = '';
continue;
}
// Variable declaration
if (preg_match('/^\s*(--[\w-]+)\s*:/', $line, $m)) {
$name = trim($m[1]);
if (isset($userVars[$name])) {
// Use the user's custom value
$output[] = $userVars[$name];
} elseif (isset($missing[$name])) {
// New variable — use starter default
$output[] = $missing[$name];
}
continue;
}
// Other lines (e.g. color-scheme) — include as-is
$output[] = $line;
}
return implode("\n", $output);
}
/**
* Replace the content inside :root { ... } with new content.
*/
private static function replaceRootBlock(string $css, string $newContent): string
{
$rootStart = preg_match('/:root[^{]*\{/', $css, $m, PREG_OFFSET_CAPTURE);
if (!$rootStart) {
return $css;
}
$openBrace = $m[0][1] + strlen($m[0][0]);
$closeBrace = self::findRootClosingBrace($css);
if ($closeBrace === false) {
return $css;
}
return substr($css, 0, $openBrace) . "\n" . $newContent . "\n" . substr($css, $closeBrace);
}
/** /**
* Extract CSS custom property declarations with their full text (name: value). * Extract CSS custom property declarations with their full text (name: value).
* Only extracts from the first :root block. * Only extracts from the first :root block.
@@ -275,25 +181,29 @@ final class MokoCssVarSync
{ {
$lines = file($starterPath, FILE_IGNORE_NEW_LINES); $lines = file($starterPath, FILE_IGNORE_NEW_LINES);
$section = 'Uncategorised'; $section = 'Uncategorised';
$map = []; // variable name => section
// Walk the starter file in order — this preserves the original
// variable ordering so injected variables match the standard theme layout.
$sections = [];
foreach ($lines as $line) { foreach ($lines as $line) {
// Detect section comment headers like /* ===== HERO VARIANTS ===== */ // Detect section comment headers like /* ===== HERO VARIANTS ===== */
if (preg_match('/\/\*\s*=+\s*(.+?)\s*=+\s*\*\//', $line, $m)) { if (preg_match('/\/\*\s*=+\s*(.+?)\s*=+\s*\*\//', $line, $m)) {
$section = trim($m[1]); $section = trim($m[1]);
} }
// Detect variable declaration — only include if it's missing from user file // Detect variable declaration
if (preg_match('/^\s*(--[\w-]+)\s*:/', $line, $m)) { if (preg_match('/^\s*(--[\w-]+)\s*:/', $line, $m)) {
$name = trim($m[1]); $name = trim($m[1]);
if (isset($missing[$name])) { if (isset($missing[$name])) {
$sections[$section][] = $missing[$name]; $map[$name] = $section;
} }
} }
} }
// Group by section
$sections = [];
foreach ($missing as $name => $declaration) {
$sec = $map[$name] ?? 'Uncategorised';
$sections[$sec][] = $declaration;
}
return $sections; return $sections;
} }

View File

@@ -39,13 +39,13 @@
</server> </server>
</updateservers> </updateservers>
<name>MokoCassiopeia</name> <name>MokoCassiopeia</name>
<version>03.10.23</version> <version>03.09.14</version>
<scriptfile>script.php</scriptfile> <scriptfile>script.php</scriptfile>
<creationDate>2026-04-19</creationDate> <creationDate>2026-04-14</creationDate>
<author>Jonathan Miller || Moko Consulting</author> <author>Jonathan Miller || Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright> <copyright>(C)GNU General Public License Version 3 - 2026 Moko Consulting</copyright>
<description><![CDATA[<div style="padding:1.25rem;border:2px solid #dc2626;border-radius:8px;background:#fef2f2;margin-bottom:1rem;"> <h3 style="color:#991b1b;margin:0 0 .75rem;">⚠️ MokoCassiopeia has been renamed to MokoOnyx</h3> <p style="margin:0 0 1rem;"> <strong>This template is no longer maintained.</strong> Please install MokoOnyx manually to continue receiving updates. MokoOnyx has the same features and will automatically import your MokoCassiopeia settings on first install. </p> <p style="margin:0 0 1rem;"> <strong>Steps to migrate:</strong> </p> <ol style="margin:0 0 1rem 1.5rem;"> <li>Download MokoOnyx from <a href="https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/v01" style="color:#2563eb;font-weight:600;">Gitea Releases (v01)</a></li> <li>Install via <strong>System → Install → Extensions → Upload Package File</strong></li> <li>Set MokoOnyx as default in <strong>System → Site Templates</strong></li> <li>Visit any frontend page — your settings are imported automatically</li> <li>Uninstall MokoCassiopeia from <strong>Extensions → Manage</strong></li> </ol> <p style="margin:0;"> <a href="https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases" style="display:inline-block;padding:.5rem 1.25rem;background:#2563eb;color:#fff;border-radius:6px;text-decoration:none;font-weight:600;">Download MokoOnyx</a> </p></div> <p><img src="https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&amp;logoColor=white" alt="License" /> <img src="https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&amp;logoColor=white" alt="Joomla" /> <img src="https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&amp;logoColor=white" alt="PHP" /></p> <p> <strong>MokoCassiopeia</strong> is succeeded by <strong>MokoOnyx</strong> — same features, new name. All future development continues under the <a href="https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx">MokoOnyx project</a>. </p>]]></description> <description><![CDATA[<p><img src="https://img.shields.io/badge/version-03.09.14-blue.svg?logo=v&amp;logoColor=white" alt="Version 03.09.14" /> <img src="https://img.shields.io/badge/license-GPL--3.0--or--later-green.svg?logo=gnu&amp;logoColor=white" alt="License" /> <img src="https://img.shields.io/badge/Joomla-5.x%20%7C%206.x-red.svg?logo=joomla&amp;logoColor=white" alt="Joomla" /> <img src="https://img.shields.io/badge/PHP-8.1%2B-777BB4.svg?logo=php&amp;logoColor=white" alt="PHP" /></p> <h3>MokoCassiopeia Template Description</h3> <p> <strong>MokoCassiopeia</strong> continues Joomla's tradition of space-themed default templates— building on the legacy of <em>Solarflare</em> (Joomla 1.0), <em>Milkyway</em> (Joomla 1.5), and <em>Protostar</em> (Joomla 3.0). </p> <p> This template is a customized fork of the <strong>Cassiopeia</strong> template introduced in Joomla 4, preserving its modern, accessible, and mobile-first foundation while introducing new stylistic enhancements and structural refinements specifically tailored for use by Moko Consulting. </p> <h4>Custom Colour Themes</h4> <p> Starter palette files are included with the template. To create a custom colour scheme, copy <code>templates/mokocassiopeia/templates/light.custom.css</code> to <code>media/templates/site/mokocassiopeia/css/theme/light.custom.css</code>, or <code>templates/mokocassiopeia/templates/dark.custom.css</code> to <code>media/templates/site/mokocassiopeia/css/theme/dark.custom.css</code>. Customise the CSS variables to match your brand, then activate your palette in <em>System → Site Templates → MokoCassiopeia → Theme tab</em> by selecting "Custom" for the Light or Dark Mode Palette. A full variable reference is available in the <em>CSS Variables</em> tab in template options. </p> <h4>Custom CSS &amp; JavaScript</h4> <p> For site-specific styles and scripts that should survive template updates, create the following files: </p> <ul> <li><code>media/templates/site/mokocassiopeia/css/user.css</code> — loaded on every page for custom CSS overrides.</li> <li><code>media/templates/site/mokocassiopeia/js/user.js</code> — loaded on every page for custom JavaScript.</li> </ul> <p> These files are gitignored and will not be overwritten by template updates. </p> <h4>Code Attribution</h4> <p> This template is based on the original <strong>Cassiopeia</strong> template developed by the <a href="https://www.joomla.org" target="_blank" rel="noopener">Joomla! Project</a> and released under the GNU General Public License. </p> <p> Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards. </p> <p> It includes integration with <a href="https://afeld.github.io/bootstrap-toc/" target="_blank" rel="noopener">Bootstrap TOC</a>, an open-source table of contents generator by A. Feld, licensed under the MIT License. </p> <p> All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable. </p>]]></description>
<inheritable>1</inheritable> <inheritable>1</inheritable>
<files> <files>
<filename>component.php</filename> <filename>component.php</filename>
@@ -295,8 +295,7 @@
<option value="0">JNO</option> <option value="0">JNO</option>
<option value="1">JYES</option> <option value="1">JYES</option>
</field> </field>
<!-- Position forced to bottom-right in index.php --> <field name="a11y_toolbar_pos" type="list" default="tl"
<field name="a11y_toolbar_pos" type="hidden" default="br"
label="TPL_MOKO_A11Y_TOOLBAR_POS" description="TPL_MOKO_A11Y_TOOLBAR_POS_DESC" label="TPL_MOKO_A11Y_TOOLBAR_POS" description="TPL_MOKO_A11Y_TOOLBAR_POS_DESC"
showon="a11y_toolbar_enabled:1"> showon="a11y_toolbar_enabled:1">
<option value="br">Bottom-right</option> <option value="br">Bottom-right</option>
@@ -313,8 +312,7 @@
<option value="0">JNO</option> <option value="0">JNO</option>
<option value="1">JYES</option> <option value="1">JYES</option>
</field> </field>
<!-- Position forced to bottom-right in index.php --> <field name="theme_fab_pos" type="list" default="br"
<field name="theme_fab_pos" type="hidden" default="br"
label="TPL_MOKO_THEME_FAB_POS" description="TPL_MOKO_THEME_FAB_POS_DESC"> label="TPL_MOKO_THEME_FAB_POS" description="TPL_MOKO_THEME_FAB_POS_DESC">
<option value="br">Bottom-right</option> <option value="br">Bottom-right</option>
<option value="bl">Bottom-left</option> <option value="bl">Bottom-left</option>
@@ -367,7 +365,6 @@
<field name="css_vars_offcanvas" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_OFFCANVAS_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_OFFCANVAS_DESC" class="alert alert-light" /> <field name="css_vars_offcanvas" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_OFFCANVAS_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_OFFCANVAS_DESC" class="alert alert-light" />
<field name="css_vars_virtuemart" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_VM_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_VM_DESC" class="alert alert-light" /> <field name="css_vars_virtuemart" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_VM_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_VM_DESC" class="alert alert-light" />
<field name="css_vars_gable" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC" class="alert alert-light" /> <field name="css_vars_gable" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_GABLE_DESC" class="alert alert-light" />
<field name="css_vars_footer" type="note" label="TPL_MOKOCASSIOPEIA_CSS_VARS_FOOTER_LABEL" description="TPL_MOKOCASSIOPEIA_CSS_VARS_FOOTER_DESC" class="alert alert-light" />
</fieldset> </fieldset>
<!-- Theme Preview tab — embedded test sheet --> <!-- Theme Preview tab — embedded test sheet -->
@@ -375,6 +372,12 @@
<field name="theme_preview_intro" type="note" description="TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO" /> <field name="theme_preview_intro" type="note" description="TPL_MOKOCASSIOPEIA_THEME_PREVIEW_INTRO" />
<field name="theme_preview_frame" type="note" description="TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME" /> <field name="theme_preview_frame" type="note" description="TPL_MOKOCASSIOPEIA_THEME_PREVIEW_FRAME" />
</fieldset> </fieldset>
<!-- Brand Showcase tab — color system, gradients, interactive sampler -->
<fieldset name="brand_showcase" label="TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_FIELDSET_LABEL">
<field name="brand_showcase_intro" type="note" description="TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_INTRO" />
<field name="brand_showcase_frame" type="note" description="TPL_MOKOCASSIOPEIA_BRAND_SHOWCASE_FRAME" />
</fieldset>
</fields> </fields>
</config> </config>
</extension> </extension>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

327
templates/dark.custom.css Normal file
View File

@@ -0,0 +1,327 @@
@charset "UTF-8";
/* 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: Joomla.Template.Site
INGROUP: MokoCassiopeia.Templates
PATH: ./templates/dark.custom.css
VERSION: 03.09.03
BRIEF: Custom dark theme color template with Bootstrap button definitions
*/
/* -----------------------------------------------
* CUSTOM DARK THEME TEMPLATE
*
* Copy this file to:
* src/media/css/theme/dark.custom.css
*
* Then register it in src/joomla.asset.json under
* template.dark.custom asset.
* --------------------------------------------- */
:root[data-bs-theme='dark'] {
/* ===== BRAND COLORS (Customize these) ===== */
--color-primary: #3399ff;
--accent-color-primary: #66b3ff;
--accent-color-secondary: #99ccff;
/* ===== LINKS ===== */
--link-color: #6bb3ff;
--link-hover-color: #99ccff;
/* ===== BODY & TYPOGRAPHY ===== */
--body-color: #e9ecef;
--body-bg: #0e1318;
/* ===== BOOTSTRAP STATE COLORS ===== */
--success: #5cb85c;
--info: #5bc0de;
--warning: #ffc107;
--danger: #d9534f;
/* ===== NAVIGATION ===== */
--nav-bg-color: var(--color-primary);
--nav-text-color: #ffffff;
}
/* ===== BOOTSTRAP BUTTON VARIANTS ===== */
.btn-primary {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--color-primary);
--btn-border-color: var(--color-primary);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #2680e6;
--btn-hover-border-color: #1f73d9;
--btn-focus-shadow-rgb: 82, 168, 255;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #1f73d9;
--btn-active-border-color: #1a66cc;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--color-primary);
--btn-disabled-border-color: var(--color-primary);
}
.btn-secondary {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: #6c757d;
--btn-border-color: #6c757d;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #5c636a;
--btn-hover-border-color: #565e64;
--btn-focus-shadow-rgb: 130, 138, 145;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #565e64;
--btn-active-border-color: #51585e;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: #6c757d;
--btn-disabled-border-color: #6c757d;
}
.btn-success {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--success);
--btn-border-color: var(--success);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #4cae4c;
--btn-hover-border-color: #449d44;
--btn-focus-shadow-rgb: 113, 198, 113;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #449d44;
--btn-active-border-color: #398439;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--success);
--btn-disabled-border-color: var(--success);
}
.btn-info {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--info);
--btn-border-color: var(--info);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #46b8da;
--btn-hover-border-color: #31b0d5;
--btn-focus-shadow-rgb: 116, 204, 233;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #31b0d5;
--btn-active-border-color: #269abc;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--info);
--btn-disabled-border-color: var(--info);
}
.btn-warning {
--btn-color: hsl(0, 0%, 0%);
--btn-bg: var(--warning);
--btn-border-color: var(--warning);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #edb100;
--btn-hover-border-color: #d39e00;
--btn-focus-shadow-rgb: 222, 170, 12;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #d39e00;
--btn-active-border-color: #c69500;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 0%);
--btn-disabled-bg: var(--warning);
--btn-disabled-border-color: var(--warning);
}
.btn-danger {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--danger);
--btn-border-color: var(--danger);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #d43f3a;
--btn-hover-border-color: #c9302c;
--btn-focus-shadow-rgb: 223, 109, 105;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #c9302c;
--btn-active-border-color: #ac2925;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--danger);
--btn-disabled-border-color: var(--danger);
}
.btn-light {
--btn-color: hsl(0, 0%, 0%);
--btn-bg: #e9ecef;
--btn-border-color: #e9ecef;
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #d3d6d9;
--btn-hover-border-color: #c8cbcf;
--btn-focus-shadow-rgb: 204, 207, 210;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #c8cbcf;
--btn-active-border-color: #bdc1c5;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 0%);
--btn-disabled-bg: #e9ecef;
--btn-disabled-border-color: #e9ecef;
}
.btn-dark {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: #2c3136;
--btn-border-color: #2c3136;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #1e2124;
--btn-hover-border-color: #191c1f;
--btn-focus-shadow-rgb: 70, 75, 80;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #191c1f;
--btn-active-border-color: #14161a;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: #2c3136;
--btn-disabled-border-color: #2c3136;
}
/* ===== OUTLINE BUTTON VARIANTS ===== */
.btn-outline-primary {
--btn-color: var(--color-primary);
--btn-border-color: var(--color-primary);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: var(--color-primary);
--btn-hover-border-color: var(--color-primary);
--btn-focus-shadow-rgb: 82, 168, 255;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: var(--color-primary);
--btn-active-border-color: var(--color-primary);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--color-primary);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--color-primary);
--gradient: none;
}
.btn-outline-secondary {
--btn-color: #8c959f;
--btn-border-color: #8c959f;
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #8c959f;
--btn-hover-border-color: #8c959f;
--btn-focus-shadow-rgb: 150, 158, 167;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #8c959f;
--btn-active-border-color: #8c959f;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: #8c959f;
--btn-disabled-bg: transparent;
--btn-disabled-border-color: #8c959f;
--gradient: none;
}
.btn-outline-success {
--btn-color: var(--success);
--btn-border-color: var(--success);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: var(--success);
--btn-hover-border-color: var(--success);
--btn-focus-shadow-rgb: 113, 198, 113;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: var(--success);
--btn-active-border-color: var(--success);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--success);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--success);
--gradient: none;
}
.btn-outline-info {
--btn-color: var(--info);
--btn-border-color: var(--info);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: var(--info);
--btn-hover-border-color: var(--info);
--btn-focus-shadow-rgb: 116, 204, 233;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: var(--info);
--btn-active-border-color: var(--info);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--info);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--info);
--gradient: none;
}
.btn-outline-warning {
--btn-color: var(--warning);
--btn-border-color: var(--warning);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: var(--warning);
--btn-hover-border-color: var(--warning);
--btn-focus-shadow-rgb: 222, 170, 12;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: var(--warning);
--btn-active-border-color: var(--warning);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--warning);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--warning);
--gradient: none;
}
.btn-outline-danger {
--btn-color: var(--danger);
--btn-border-color: var(--danger);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: var(--danger);
--btn-hover-border-color: var(--danger);
--btn-focus-shadow-rgb: 223, 109, 105;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: var(--danger);
--btn-active-border-color: var(--danger);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--danger);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--danger);
--gradient: none;
}
.btn-outline-light {
--btn-color: #e9ecef;
--btn-border-color: #e9ecef;
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #e9ecef;
--btn-hover-border-color: #e9ecef;
--btn-focus-shadow-rgb: 204, 207, 210;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #e9ecef;
--btn-active-border-color: #e9ecef;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: #e9ecef;
--btn-disabled-bg: transparent;
--btn-disabled-border-color: #e9ecef;
--gradient: none;
}
.btn-outline-dark {
--btn-color: #4a5057;
--btn-border-color: #4a5057;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #4a5057;
--btn-hover-border-color: #4a5057;
--btn-focus-shadow-rgb: 90, 95, 100;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #4a5057;
--btn-active-border-color: #4a5057;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: #4a5057;
--btn-disabled-bg: transparent;
--btn-disabled-border-color: #4a5057;
--gradient: none;
}

327
templates/light.custom.css Normal file
View File

@@ -0,0 +1,327 @@
@charset "UTF-8";
/* 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: Joomla.Template.Site
INGROUP: MokoCassiopeia.Templates
PATH: ./templates/light.custom.css
VERSION: 03.09.03
BRIEF: Custom light theme color template with Bootstrap button definitions
*/
/* -----------------------------------------------
* CUSTOM LIGHT THEME TEMPLATE
*
* Copy this file to:
* src/media/css/theme/light.custom.css
*
* Then register it in src/joomla.asset.json under
* template.light.custom asset.
* --------------------------------------------- */
:root[data-bs-theme="light"] {
/* ===== BRAND COLORS (Customize these) ===== */
--color-primary: #0066cc;
--accent-color-primary: #3399ff;
--accent-color-secondary: #66b3ff;
/* ===== LINKS ===== */
--link-color: #0066cc;
--link-hover-color: #0052a3;
/* ===== BODY & TYPOGRAPHY ===== */
--body-color: #212529;
--body-bg: #ffffff;
/* ===== BOOTSTRAP STATE COLORS ===== */
--success: #28a745;
--info: #17a2b8;
--warning: #ffc107;
--danger: #dc3545;
/* ===== NAVIGATION ===== */
--nav-bg-color: var(--color-primary);
--nav-text-color: #ffffff;
}
/* ===== BOOTSTRAP BUTTON VARIANTS ===== */
.btn-primary {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--color-primary);
--btn-border-color: var(--color-primary);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #0052a3;
--btn-hover-border-color: #004d99;
--btn-focus-shadow-rgb: 38, 128, 217;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #004d99;
--btn-active-border-color: #004788;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--color-primary);
--btn-disabled-border-color: var(--color-primary);
}
.btn-secondary {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: #6c757d;
--btn-border-color: #6c757d;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #5c636a;
--btn-hover-border-color: #565e64;
--btn-focus-shadow-rgb: 130, 138, 145;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #565e64;
--btn-active-border-color: #51585e;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: #6c757d;
--btn-disabled-border-color: #6c757d;
}
.btn-success {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--success);
--btn-border-color: var(--success);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #218838;
--btn-hover-border-color: #1e7e34;
--btn-focus-shadow-rgb: 72, 180, 97;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #1e7e34;
--btn-active-border-color: #1c7430;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--success);
--btn-disabled-border-color: var(--success);
}
.btn-info {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--info);
--btn-border-color: var(--info);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #138496;
--btn-hover-border-color: #117a8b;
--btn-focus-shadow-rgb: 58, 176, 195;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #117a8b;
--btn-active-border-color: #10707f;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--info);
--btn-disabled-border-color: var(--info);
}
.btn-warning {
--btn-color: hsl(0, 0%, 0%);
--btn-bg: var(--warning);
--btn-border-color: var(--warning);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #e0a800;
--btn-hover-border-color: #d39e00;
--btn-focus-shadow-rgb: 222, 170, 12;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #d39e00;
--btn-active-border-color: #c69500;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 0%);
--btn-disabled-bg: var(--warning);
--btn-disabled-border-color: var(--warning);
}
.btn-danger {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: var(--danger);
--btn-border-color: var(--danger);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #c82333;
--btn-hover-border-color: #bd2130;
--btn-focus-shadow-rgb: 225, 83, 97;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #bd2130;
--btn-active-border-color: #b21f2d;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: var(--danger);
--btn-disabled-border-color: var(--danger);
}
.btn-light {
--btn-color: hsl(0, 0%, 0%);
--btn-bg: #f8f9fa;
--btn-border-color: #f8f9fa;
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #e2e6ea;
--btn-hover-border-color: #dae0e5;
--btn-focus-shadow-rgb: 216, 217, 219;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #dae0e5;
--btn-active-border-color: #d3d9df;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 0%);
--btn-disabled-bg: #f8f9fa;
--btn-disabled-border-color: #f8f9fa;
}
.btn-dark {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: #343a40;
--btn-border-color: #343a40;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #23272b;
--btn-hover-border-color: #1d2124;
--btn-focus-shadow-rgb: 82, 88, 93;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #1d2124;
--btn-active-border-color: #171a1d;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: hsl(0, 0%, 100%);
--btn-disabled-bg: #343a40;
--btn-disabled-border-color: #343a40;
}
/* ===== OUTLINE BUTTON VARIANTS ===== */
.btn-outline-primary {
--btn-color: var(--color-primary);
--btn-border-color: var(--color-primary);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: var(--color-primary);
--btn-hover-border-color: var(--color-primary);
--btn-focus-shadow-rgb: 38, 128, 217;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: var(--color-primary);
--btn-active-border-color: var(--color-primary);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--color-primary);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--color-primary);
--gradient: none;
}
.btn-outline-secondary {
--btn-color: #6c757d;
--btn-border-color: #6c757d;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #6c757d;
--btn-hover-border-color: #6c757d;
--btn-focus-shadow-rgb: 130, 138, 145;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #6c757d;
--btn-active-border-color: #6c757d;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: #6c757d;
--btn-disabled-bg: transparent;
--btn-disabled-border-color: #6c757d;
--gradient: none;
}
.btn-outline-success {
--btn-color: var(--success);
--btn-border-color: var(--success);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: var(--success);
--btn-hover-border-color: var(--success);
--btn-focus-shadow-rgb: 72, 180, 97;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: var(--success);
--btn-active-border-color: var(--success);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--success);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--success);
--gradient: none;
}
.btn-outline-info {
--btn-color: var(--info);
--btn-border-color: var(--info);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: var(--info);
--btn-hover-border-color: var(--info);
--btn-focus-shadow-rgb: 58, 176, 195;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: var(--info);
--btn-active-border-color: var(--info);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--info);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--info);
--gradient: none;
}
.btn-outline-warning {
--btn-color: var(--warning);
--btn-border-color: var(--warning);
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: var(--warning);
--btn-hover-border-color: var(--warning);
--btn-focus-shadow-rgb: 222, 170, 12;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: var(--warning);
--btn-active-border-color: var(--warning);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--warning);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--warning);
--gradient: none;
}
.btn-outline-danger {
--btn-color: var(--danger);
--btn-border-color: var(--danger);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: var(--danger);
--btn-hover-border-color: var(--danger);
--btn-focus-shadow-rgb: 225, 83, 97;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: var(--danger);
--btn-active-border-color: var(--danger);
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: var(--danger);
--btn-disabled-bg: transparent;
--btn-disabled-border-color: var(--danger);
--gradient: none;
}
.btn-outline-light {
--btn-color: #f8f9fa;
--btn-border-color: #f8f9fa;
--btn-hover-color: hsl(0, 0%, 0%);
--btn-hover-bg: #f8f9fa;
--btn-hover-border-color: #f8f9fa;
--btn-focus-shadow-rgb: 216, 217, 219;
--btn-active-color: hsl(0, 0%, 0%);
--btn-active-bg: #f8f9fa;
--btn-active-border-color: #f8f9fa;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: #f8f9fa;
--btn-disabled-bg: transparent;
--btn-disabled-border-color: #f8f9fa;
--gradient: none;
}
.btn-outline-dark {
--btn-color: #343a40;
--btn-border-color: #343a40;
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #343a40;
--btn-hover-border-color: #343a40;
--btn-focus-shadow-rgb: 82, 88, 93;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #343a40;
--btn-active-border-color: #343a40;
--btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--btn-disabled-color: #343a40;
--btn-disabled-bg: transparent;
--btn-disabled-border-color: #343a40;
--gradient: none;
}

39
update.xml Normal file
View File

@@ -0,0 +1,39 @@
<!--
Joomla Extension Update Server XML
See: https://docs.joomla.org/Deploying_an_Update_Server
This file is the update server manifest for {{EXTENSION_NAME}}.
The Joomla installer polls this URL to check for new versions.
The manifest.xml in this repository must reference this file:
<updateservers>
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
https://git.mokoconsulting.tech/mokoconsulting-tech/MokoCassiopeia/raw/branch/main/update.xml
</server>
<server type="extension" priority="2" name="{{EXTENSION_NAME}}">
https://raw.githubusercontent.com/mokoconsulting-tech/MokoCassiopeia/main/update.xml
</server>
</updateservers>
When a new release is made, run `make release` or the release workflow to
prepend a new <update> entry to this file automatically.
-->
<updates>
<update>
<name>{{EXTENSION_NAME}}</name>
<description>MokoCassiopeia — Moko Consulting Joomla extension</description>
<element>{{EXTENSION_ELEMENT}}</element>
<type>{{EXTENSION_TYPE}}</type>
<version>{{VERSION}}</version>
<downloads>
<downloadurl type="full" format="zip">
https://git.mokoconsulting.tech/mokoconsulting-tech/MokoCassiopeia/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
</downloadurl>
<downloadurl type="full" format="zip">
https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="[56].*"/>
<php_minimum>8.1</php_minimum>
</update>
</updates>

View File

@@ -1,28 +1,25 @@
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> <!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 03.10.23 VERSION: 03.09.16
NOTE: This repository is RETIRED. All channels point to the same final stable release.
All future development is at https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
--> -->
<updates> <updates>
<!-- All channels point to the same final release so every site sees the update --> <!-- 1. DEVELOPMENT — dev → -->
<update> <update>
<name>MokoCassiopeia</name> <name>MokoCassiopeia</name>
<description>MokoCassiopeia is retired. Please install MokoOnyx instead.</description> <description>MokoCassiopeia development build — unstable.</description>
<element>mokocassiopeia</element> <element>mokocassiopeia</element>
<type>template</type> <type>template</type>
<client>site</client> <client>site</client>
<version>03.10.23</version> <version>03.09.16</version>
<creationDate>2026-04-21</creationDate> <creationDate>2026-04-17</creationDate>
<infourl title='MokoCassiopeia'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03</infourl> <infourl title='MokoCassiopeia Dev'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/development</infourl>
<downloads> <downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.23.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/development/mokocassiopeia-03.09.16-dev.zip</downloadurl>
</downloads> </downloads>
<sha256>314ead3bafbaea370796b7ed9d8353ae9964becbf7ccf9be09e94229973440fc</sha256> <sha256>sha256:2986f08b59617a18d489e0d9e6e49d329ceb8297ae4755b6697f3326c2a41fc4</sha256>
<tags><tag>development</tag></tags> <tags><tag>development</tag></tags>
<maintainer>Moko Consulting</maintainer> <maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl> <maintainerurl>https://mokoconsulting.tech</maintainerurl>
@@ -30,19 +27,20 @@
<php_minimum>8.1</php_minimum> <php_minimum>8.1</php_minimum>
</update> </update>
<!-- 2. ALPHA — dev → alpha → -->
<update> <update>
<name>MokoCassiopeia</name> <name>MokoCassiopeia</name>
<description>MokoCassiopeia is retired. Please install MokoOnyx instead.</description> <description>MokoCassiopeia alpha build — early testing.</description>
<element>mokocassiopeia</element> <element>mokocassiopeia</element>
<type>template</type> <type>template</type>
<client>site</client> <client>site</client>
<version>03.10.23</version> <version>03.09.16</version>
<creationDate>2026-04-21</creationDate> <creationDate>2026-04-14</creationDate>
<infourl title='MokoCassiopeia'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03</infourl> <infourl title='MokoCassiopeia Alpha'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/alpha</infourl>
<downloads> <downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.23.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/alpha/mokocassiopeia-03.09.16-alpha.zip</downloadurl>
</downloads> </downloads>
<sha256>314ead3bafbaea370796b7ed9d8353ae9964becbf7ccf9be09e94229973440fc</sha256> <sha256>c2660acdf7389244462485f7ab4c286e9f851366a148acc16739a184576f7932</sha256>
<tags><tag>alpha</tag></tags> <tags><tag>alpha</tag></tags>
<maintainer>Moko Consulting</maintainer> <maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl> <maintainerurl>https://mokoconsulting.tech</maintainerurl>
@@ -50,19 +48,20 @@
<php_minimum>8.1</php_minimum> <php_minimum>8.1</php_minimum>
</update> </update>
<!-- 3. BETA — dev → alpha → beta → -->
<update> <update>
<name>MokoCassiopeia</name> <name>MokoCassiopeia</name>
<description>MokoCassiopeia is retired. Please install MokoOnyx instead.</description> <description>MokoCassiopeia beta build — feature complete, stability testing.</description>
<element>mokocassiopeia</element> <element>mokocassiopeia</element>
<type>template</type> <type>template</type>
<client>site</client> <client>site</client>
<version>03.10.23</version> <version>03.09.16</version>
<creationDate>2026-04-21</creationDate> <creationDate>2026-04-14</creationDate>
<infourl title='MokoCassiopeia'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03</infourl> <infourl title='MokoCassiopeia Beta'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/beta</infourl>
<downloads> <downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.23.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/beta/mokocassiopeia-03.09.16-beta.zip</downloadurl>
</downloads> </downloads>
<sha256>314ead3bafbaea370796b7ed9d8353ae9964becbf7ccf9be09e94229973440fc</sha256> <sha256>4cbe4fc379182ef17580396e7d12ce4ce95a90017ef364b922bdc2d04b0b3d97</sha256>
<tags><tag>beta</tag></tags> <tags><tag>beta</tag></tags>
<maintainer>Moko Consulting</maintainer> <maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl> <maintainerurl>https://mokoconsulting.tech</maintainerurl>
@@ -70,19 +69,21 @@
<php_minimum>8.1</php_minimum> <php_minimum>8.1</php_minimum>
</update> </update>
<!-- 4. RC — dev → alpha → beta → rc → -->
<update> <update>
<name>MokoCassiopeia</name> <name>MokoCassiopeia</name>
<description>MokoCassiopeia is retired. Please install MokoOnyx instead.</description> <description>MokoCassiopeia release candidate — testing only.</description>
<element>mokocassiopeia</element> <element>mokocassiopeia</element>
<type>template</type> <type>template</type>
<client>site</client> <client>site</client>
<version>03.10.23</version> <version>03.09.16</version>
<creationDate>2026-04-21</creationDate> <creationDate>2026-04-14</creationDate>
<infourl title='MokoCassiopeia'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03</infourl> <infourl title='MokoCassiopeia RC'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/release-candidate</infourl>
<downloads> <downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.23.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-03.09.16-rc.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/release-candidate/mokocassiopeia-03.09.16-rc.zip</downloadurl>
</downloads> </downloads>
<sha256>314ead3bafbaea370796b7ed9d8353ae9964becbf7ccf9be09e94229973440fc</sha256> <sha256>c2660acdf7389244462485f7ab4c286e9f851366a148acc16739a184576f7932</sha256>
<tags><tag>rc</tag></tags> <tags><tag>rc</tag></tags>
<maintainer>Moko Consulting</maintainer> <maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl> <maintainerurl>https://mokoconsulting.tech</maintainerurl>
@@ -90,19 +91,21 @@
<php_minimum>8.1</php_minimum> <php_minimum>8.1</php_minimum>
</update> </update>
<!-- 5. STABLE — dev → alpha → beta → rc → version/XX → main -->
<update> <update>
<name>MokoCassiopeia</name> <name>MokoCassiopeia</name>
<description>MokoCassiopeia is retired. Please install MokoOnyx instead.</description> <description>Moko Consulting's site template based on Cassiopeia.</description>
<element>mokocassiopeia</element> <element>mokocassiopeia</element>
<type>template</type> <type>template</type>
<client>site</client> <client>site</client>
<version>03.10.23</version> <version>03.09.16</version>
<creationDate>2026-04-21</creationDate> <creationDate>2026-04-14</creationDate>
<infourl title='MokoCassiopeia'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03</infourl> <infourl title='MokoCassiopeia'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/tag/v03</infourl>
<downloads> <downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.10.23.zip</downloadurl> <downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.09.16.zip</downloadurl>
<downloadurl type='full' format='zip'>https://github.com/mokoconsulting-tech/MokoCassiopeia/releases/download/v03/mokocassiopeia-03.09.16.zip</downloadurl>
</downloads> </downloads>
<sha256>314ead3bafbaea370796b7ed9d8353ae9964becbf7ccf9be09e94229973440fc</sha256> <sha256>c2660acdf7389244462485f7ab4c286e9f851366a148acc16739a184576f7932</sha256>
<tags><tag>stable</tag></tags> <tags><tag>stable</tag></tags>
<maintainer>Moko Consulting</maintainer> <maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl> <maintainerurl>https://mokoconsulting.tech</maintainerurl>