Compare commits

...

72 Commits

Author SHA1 Message Date
jmiller 49f9a762df chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:57:44 +00:00
jmiller d46325e13b chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:40:21 +00:00
gitea-actions[bot] 84b3b9acb1 chore(release): build 01.12.00 [skip ci] 2026-06-04 15:36:31 +00:00
jmiller 9f0c01739f Merge pull request 'chore: remove updates.xml - update server migrated' (#62) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
2026-06-04 15:36:20 +00:00
Jonathan Miller a7eabcf9d7 chore: remove updates.xml and update.xml — update server migrated
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 6s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 10s
Update server now managed externally. Remove local updates.xml and
legacy update.xml files.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 10:34:20 -05:00
jmiller 724c4d61f4 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:31:37 +00:00
jmiller 35cb0a988c chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:28:39 +00:00
jmiller 9e27c5c167 chore: remove static updates.xml [skip ci] 2026-06-04 15:22:59 +00:00
jmiller f56bfa6c36 chore: sync updates.xml 01.11.00 from main [skip ci] 2026-06-04 15:22:44 +00:00
gitea-actions[bot] f4dfcc4c7e chore: update channels for 01.11.00 [skip ci] 2026-06-04 15:22:43 +00:00
gitea-actions[bot] f9187cd816 chore(release): build 01.11.00 [skip ci] 2026-06-04 15:22:41 +00:00
jmiller a9d4534f0b Merge pull request 'chore: clean up updates.xml for stable 01.10.00' (#61) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-04 15:22:33 +00:00
Jonathan Miller a922ef3033 chore: merge main into dev, resolve updates.xml conflicts
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Successful in 4s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 11s
2026-06-04 10:21:47 -05:00
jmiller 9fbc273658 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:18:27 +00:00
Jonathan Miller 95319c0513 chore: clean up updates.xml — remove stale entries, keep stable + legacy migration
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 5s
Universal: Build & Release / Promote to RC (pull_request) Failing after 7s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Remove old dev/alpha/beta/rc entries. Keep current stable 01.10.00
package entry and legacy mod_mokojoomhero entry pointing to the
package for migration from old standalone module installs.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 10:14:36 -05:00
jmiller 10412c1120 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-04 15:13:06 +00:00
gitea-actions[bot] ab82a8f810 chore: update channels for 01.10.00 [skip ci] 2026-06-04 15:03:47 +00:00
jmiller 19db4cb637 chore: sync updates.xml 01.10.00 from main [skip ci] 2026-06-04 15:03:47 +00:00
gitea-actions[bot] 28bc4f042a chore(release): build 01.10.00 [skip ci] 2026-06-04 15:03:45 +00:00
jmiller 9de47e9fe2 Merge pull request 'feat: scheduling, A/B testing, repo metadata update' (#60) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
2026-06-04 15:03:36 +00:00
Jonathan Miller af3acc6fd2 feat: scheduling and A/B testing (#21, #22)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 4s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Universal: PR Check / Validate PR (pull_request) Failing after 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 11s
Add hero scheduling with start/end datetime fields using Joomla
calendar type and site timezone. Hero skips rendering outside the
configured range. Add A/B testing with weighted random variation
selection, session-sticky per module instance via subform repeatable.
Close #23 as substantially complete (animation library implemented
in prior phases). Language strings in all locales.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 09:59:04 -05:00
jmiller ad0c5bc51c feat(update): migrate update server URL to Gitea Pages [skip ci] 2026-06-04 14:33:21 +00:00
jmiller fdf07d833f feat(update): migrate update server URL to Gitea Pages [skip ci] 2026-06-04 14:33:15 +00:00
jmiller 765086d7a0 chore: sync updates.xml 01.09.00 from main [skip ci] 2026-06-04 14:31:56 +00:00
gitea-actions[bot] 9e729c059d chore: update channels for 01.09.00 [skip ci] 2026-06-04 14:31:55 +00:00
gitea-actions[bot] d708ac995d chore(release): build 01.09.00 [skip ci] 2026-06-04 14:31:53 +00:00
jmiller 8895ba9ffb Merge pull request 'feat: restore licensing system with free tier' (#59) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Merge PR #59: restore licensing system with free tier
2026-06-04 14:31:41 +00:00
jmiller 2f88eb50fb chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-04 14:22:50 +00:00
jmiller d22ba60eea chore: sync .mokogitea/workflows/auto-release.yml from moko-platform [skip ci]
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 13s
2026-06-04 14:20:08 +00:00
Jonathan Miller 52e362a769 feat: restore licensing system with free tier (no key required)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 5s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 6s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 9s
Universal: Build & Release / Promote to RC (pull_request) Failing after 11s
Re-enable the system plugin license check with a LICENSE_TYPE constant.
Set to 'free' — check exits immediately with zero overhead. Change to
'pro' to enable download key validation. Update plugin descriptions
and CLAUDE.md to document the licensing model.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 09:14:54 -05:00
jmiller 012f8adc3d chore: sync updates.xml 01.08.00 from main [skip ci] 2026-06-04 13:15:19 +00:00
gitea-actions[bot] 5e7050576a chore: update channels for 01.08.00 [skip ci] 2026-06-04 13:15:18 +00:00
gitea-actions[bot] 95bba21c91 chore(release): build 01.08.00 [skip ci] 2026-06-04 13:15:17 +00:00
jmiller 998f62fcfb Merge pull request 'feat: package restructure, 10 feature expansions, review fixes' (#58) from dev into main
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-04 13:15:08 +00:00
Jonathan Miller ba5ae04755 chore: merge main into dev, resolve conflicts from package restructure
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 4s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 12s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: Build & Release / Promote to RC (pull_request) Has been skipped
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 1m8s
Universal: PR Check / Validate PR (pull_request) Failing after 1m7s
Universal: Build & Release / Build & Release Pipeline (pull_request) Successful in 12s
Resolve modify/delete conflicts for old src/ files that moved to
src/packages/. Keep dev workflow files. Remove deleted cascade-dev.yml.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 08:14:07 -05:00
jmiller b3b410e190 chore: sync updates.xml 01.08.00-rc from rc [skip ci] 2026-06-04 13:02:22 +00:00
jmiller c38307f98b chore: sync updates.xml 01.08.00-rc from rc [skip ci] 2026-06-04 13:02:21 +00:00
Jonathan Miller c3a4a1f28d fix: final review — logging, null guards, XSS filter, stale descriptions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Universal: Build & Release / Build & Release Pipeline (pull_request) Has been skipped
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 5s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 6s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 6s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Build & Release / Promote to RC (pull_request) Successful in 17s
Add Joomla Log::add for article query, DirectoryIterator, and JSON
decode failures. Filter article content through HTMLHelper content.prepare
to prevent XSS. Add null guards on scroll indicator and mute toggle
hero/icon elements. Add console.warn on slide content JSON parse failure.
Remove stale license key references from package and plugin descriptions.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:58:34 -05:00
Jonathan Miller 4d67a32cb0 feat: article content source, per-slide content, remove license check (#56, #57)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Add content source selector — manual editor or Joomla article with
optional article title override. Add per-slide unique content via
subform repeatable field with heading, body, link, and link text per
slide, swapped in sync with background transitions using safe DOM
methods. Remove license key check from system plugin and plugin
dependency from module — extension is now free. Language strings in
all locales.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:45:23 -05:00
Jonathan Miller 6f9aa71573 feat: content entrance animations and parallax scroll effect (#48, #52)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Add configurable content entrance animations (fade-in, slide-up,
slide-left, slide-right) triggered by IntersectionObserver on scroll
into view, with adjustable delay. Add parallax scroll effect with
configurable speed (0.1–0.9) using passive scroll listener and GPU-
accelerated transforms. Both features respect prefers-reduced-motion.
Language strings in all locales.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:37:59 -05:00
Jonathan Miller 4a18f46c68 feat: reduced motion, scroll indicator, and video poster (#49, #50, #51)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Add prefers-reduced-motion support (WCAG 2.1 AA) — disables slideshow
cycling, CSS transitions/animations, and Ken Burns zoom when OS setting
is enabled. Add optional scroll-down chevron indicator with bounce
animation and smooth-scroll click handler. Add video poster image
fallback displayed while video loads. Language strings in all locales.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:30:12 -05:00
Jonathan Miller f5fdf6742f fix: input validation, JS error handling, and package description
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Add hex color validation for all color params, allowlist validation
for textAlign/fadeType/overlayType, range clamp for gradientAngle,
and try-catch around DirectoryIterator. Fix video.play() promise
rejection and iframe.contentWindow null guards in JS. Hardcode
package description in manifest. Normalize tabs throughout.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:23:31 -05:00
Jonathan Miller fe3abf6ddb feat: vertical alignment, mobile height, and gradient overlay (#53, #54, #55)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Add vertical text alignment (top/center/bottom) for overlay content,
mobile-specific hero height via CSS custom property, and directional
gradient overlay (dark at bottom/top/left/right) reusing existing
overlay colour controls. Language strings added to all locale files.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 07:13:04 -05:00
Jonathan Miller 601cf77170 fix: address PR review issues and add configurable slide transitions
Fix CSS injection on heroHeight with regex validation, add missing
language keys to .sys.ini files, fix plugin manifest languages folder
attribute and display name, narrow catch to \Exception with logging,
add error handling to pkg_script auto-enable, fix mobile CSS for
color/gradient modes, add SPDX headers, remove dead code, and update
stale file path headers.

Add configurable transition type for image slideshow: crossfade, slide,
fade-to-black, and zoom (Ken Burns).

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 06:46:47 -05:00
Jonathan Miller 902321de47 feat: restructure as package extension with solid color and gradient hero modes
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Restructure from standalone module to package extension (pkg_mokojoomhero)
containing mod_mokojoomhero and plg_system_mokojoomhero. Add two new hero
modes — solid color and gradient — with color pickers and angle controls,
using Joomla showon for conditional field display.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-03 21:21:14 -05:00
jmiller d085c79d9e chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] 2026-06-03 09:36:44 +00:00
jmiller ae19e32407 chore: sync .mokogitea/workflows/repo-health.yml from moko-platform [skip ci] 2026-06-03 03:10:24 +00:00
jmiller 61cd30471f chore: add .mokogitea/workflows/auto-release.yml from moko-platform [skip ci] 2026-06-02 23:47:02 +00:00
jmiller 46bfeaa2e1 chore: sync .mokogitea/workflows/pr-check.yml from moko-platform [skip ci] 2026-06-02 21:51:19 +00:00
Moko Consulting ea1391ab38 chore(ci): sync CI issue reporter from Template-Joomla
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 21:32:32 +00:00
Moko Consulting fe84d02827 chore(ci): sync CI issue reporter from Template-Joomla
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 21:32:30 +00:00
Moko Consulting cfe69452db chore(ci): sync CI issue reporter from Template-Joomla
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 21:32:29 +00:00
Moko Consulting a465ee1d38 chore(ci): sync CI issue reporter from Template-Joomla
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 21:32:25 +00:00
Moko Consulting 0742f056be chore(ci): sync CI issue reporter from Template-Joomla
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 21:32:23 +00:00
Moko Consulting 1b74b9c8d7 chore(ci): sync CI issue reporter from Template-Joomla
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 21:32:21 +00:00
Moko Consulting 518f8f8850 chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:36:43 +00:00
Moko Consulting fe908b68a2 chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:36:43 +00:00
Moko Consulting b85921697f chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:36:43 +00:00
Moko Consulting 77b39a2d52 chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:36:42 +00:00
Moko Consulting 351e86328f chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:36:41 +00:00
Moko Consulting 31de00c741 chore(ci): add CI issue reporter for auto-filing gate failures
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Generic: Repo Health / Report Issues (push) Has been cancelled
2026-06-02 20:36:41 +00:00
gitea-actions[bot] 595bc94b4b chore(ci): remove update-server.yml for update server migration [skip ci] 2026-05-31 03:47:33 +00:00
gitea-actions[bot] f1049fd289 chore(ci): remove cascade-dev.yml for update server migration [skip ci] 2026-05-31 03:47:31 +00:00
gitea-actions[bot] 79627c4f1d chore(ci): remove auto-bump.yml for update server migration [skip ci] 2026-05-31 03:47:29 +00:00
gitea-actions[bot] 519c735e20 chore(ci): remove pre-release.yml for update server migration [skip ci] 2026-05-31 03:47:24 +00:00
gitea-actions[bot] bc31cad73a chore(ci): remove auto-release.yml for update server migration [skip ci] 2026-05-31 03:47:18 +00:00
jmiller c583ebd56e chore: sync .mokogitea/workflows/cascade-dev.yml from moko-platform [skip ci] 2026-05-31 01:45:07 +00:00
jmiller f9543058df chore: sync .mokogitea/workflows/cascade-dev.yml from moko-platform [skip ci] 2026-05-31 01:41:22 +00:00
jmiller 4b08e5d889 chore: sync CONTRIBUTING.md from moko-platform [skip ci] 2026-05-31 01:09:44 +00:00
Jonathan Miller 2f2832c661 chore(manifest): fix display-name structure and update CONTRIBUTING.md
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: Auto Version Bump / Version Bump (push) Failing after 4s
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
Standardize manifest.xml identity block: ensure <name> contains only
the machine identifier (PascalCase) and <display-name> contains the
human-readable label with Joomla extension type prefix. Remove duplicate
<version> tags where present. Update CONTRIBUTING.md from moko-platform
default.

Authored-by: Moko Consulting
2026-05-30 19:11:11 -05:00
gitea-actions[bot] 69e75f50fb chore: update channels for 01.07.00 [skip ci] 2026-05-30 22:25:29 +00:00
jmiller 7df3c7afd7 chore: sync updates.xml 01.07.00 from main [skip ci] 2026-05-30 22:25:29 +00:00
67 changed files with 4363 additions and 3238 deletions
+3 -2
View File
@@ -6,10 +6,11 @@
-->
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
<identity>
<name>Module - MokoJoomHero</name>
<name>MokoJoomHero</name>
<display-name>Package - MokoJoomHero</display-name>
<org>MokoConsulting</org>
<description>A Joomla Module designed to provide a random image from a folder with content on top as a Hero.</description>
<version>01.07.00</version>
<version>01.12.00</version>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
<governance>
-66
View File
@@ -1,66 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
name: "Universal: Auto Version Bump"
on:
push:
branches:
- dev
- rc
- 'feature/**'
- 'patch/**'
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
permissions:
contents: write
jobs:
bump:
name: Version Bump
runs-on: release
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request')
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
- name: Setup moko-platform tools
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
if [ -d "/opt/moko-platform/cli" ]; then
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
else
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
fi
- name: Bump version
run: |
php ${MOKO_CLI}/version_auto_bump.php \
--path . --branch "${GITHUB_REF_NAME}" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
+18 -3
View File
@@ -102,13 +102,14 @@ jobs:
run: |
php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
- name: Summary
if: always()
run: |
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
release:
@@ -131,6 +132,19 @@ jobs:
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found — aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
@@ -154,7 +168,8 @@ jobs:
run: |
php /tmp/moko-platform-api/cli/release_publish.php \
--path . --stability stable --bump minor --branch main \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--skip-update-stream
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
-213
View File
@@ -1,213 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Maintenance
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/cascade-dev.yml.template
# VERSION: 02.00.00
# BRIEF: Forward-merge main → all open branches after every push to main
#
# +========================================================================+
# | CASCADE MAIN → ALL BRANCHES |
# +========================================================================+
# | |
# | Triggers on every push to main (PR merges, bot commits, etc.) |
# | |
# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* |
# | 2. For each: create PR (main → branch), auto-merge if clean |
# | 3. On conflict: leave PR open for manual resolution |
# | |
# +========================================================================+
name: "Universal: Cascade Main → Dev"
on:
push:
branches:
- main
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
pull-requests: write
jobs:
cascade:
name: Cascade main → branches
runs-on: ubuntu-latest
if: >-
!contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip cascade]')
steps:
- name: Discover target branches
id: branches
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Fetch all branches (paginated)
PAGE=1
ALL_BRANCHES=""
while true; do
BATCH=$(curl -sS \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/branches?page=${PAGE}&limit=50" \
| jq -r '.[].name // empty')
[ -z "$BATCH" ] && break
ALL_BRANCHES="$ALL_BRANCHES $BATCH"
PAGE=$((PAGE + 1))
done
# Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/*
TARGETS=""
for BRANCH in $ALL_BRANCHES; do
case "$BRANCH" in
dev|dev/*|rc/*|beta/*|alpha/*)
TARGETS="$TARGETS $BRANCH"
;;
esac
done
TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace
if [ -z "$TARGETS" ]; then
echo "targets=" >> "$GITHUB_OUTPUT"
echo "️ No cascade target branches found"
else
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
COUNT=$(echo "$TARGETS" | wc -w)
echo "📋 Found ${COUNT} target branch(es): ${TARGETS}"
fi
- name: Cascade to all target branches
if: steps.branches.outputs.targets != ''
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: |
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
SHORT_SHA="${GITHUB_SHA:0:7}"
TARGETS="${{ steps.branches.outputs.targets }}"
SUCCESS=0
CONFLICTS=0
SKIPPED=0
FAILED=0
for BRANCH in $TARGETS; do
echo ""
echo "═══ main → ${BRANCH} ═══"
# Check if branch is already up to date
ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g')
RESPONSE=$(curl -sS \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/compare/${ENCODED_BRANCH}...main")
AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0')
if [ "$AHEAD" -eq 0 ]; then
echo " ✅ Already up to date"
SKIPPED=$((SKIPPED + 1))
continue
fi
echo " ️ main is ${AHEAD} commit(s) ahead"
# Check for existing cascade PR
EXISTING=$(curl -sS \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1")
EXISTING_COUNT=$(echo "$EXISTING" | jq 'length')
PR_NUMBER=""
if [ "$EXISTING_COUNT" -gt 0 ]; then
PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number')
echo " ️ Reusing existing PR #${PR_NUMBER}"
else
# Create cascade PR
PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\",
\"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\",
\"head\": \"main\",
\"base\": \"${BRANCH}\"
}" \
"${API}/pulls")
HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1)
BODY=$(echo "$PR_RESPONSE" | sed '$d')
PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty')
if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then
MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)
echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}"
FAILED=$((FAILED + 1))
continue
fi
echo " ✅ Created PR #${PR_NUMBER}"
fi
# Try auto-merge
PR_DATA=$(curl -sS \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/pulls/${PR_NUMBER}")
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false')
if [ "$MERGEABLE" != "true" ]; then
echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open"
CONFLICTS=$((CONFLICTS + 1))
continue
fi
MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"Do\": \"merge\",
\"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\",
\"delete_branch_after_merge\": false
}" \
"${API}/pulls/${PR_NUMBER}/merge")
MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1)
if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then
echo " ✅ Merged — ${BRANCH} is in sync"
SUCCESS=$((SUCCESS + 1))
else
MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d')
echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open"
CONFLICTS=$((CONFLICTS + 1))
fi
done
# Summary
echo ""
echo "════════════════════════════════════════"
echo " ✅ Merged: ${SUCCESS}"
echo " ⚠️ Conflicts: ${CONFLICTS}"
echo " ⏭️ Up to date: ${SKIPPED}"
echo " ❌ Failed: ${FAILED}"
echo "════════════════════════════════════════"
if [ "$FAILED" -gt 0 ]; then
exit 1
fi
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Automation
# VERSION: 01.07.00
# VERSION: 01.12.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
+508 -236
View File
@@ -1,236 +1,508 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 05.00.00
# BRIEF: PR gate — branch policy + code validation before merge
name: "Universal: PR Check"
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Branch Policy ──────────────────────────────────────────────────────
branch-policy:
name: Branch Policy
runs-on: ubuntu-latest
steps:
- name: Check branch merge target
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
patch/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
ALLOWED=false
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
rc)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="RC branch can only merge into 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Code Validation ────────────────────────────────────────────────────
validate:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Detect platform
id: platform
run: |
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: PHP syntax check
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
echo "PHP lint: ${ERRORS} error(s)"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Validate platform manifest
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (WaaS site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
fi
for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done
echo "Joomla manifest valid"
;;
dolibarr)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
if [ -z "$MOD_FILE" ]; then
echo "::error::No mod*.class.php found"
exit 1
fi
echo "Dolibarr module: ${MOD_FILE}"
;;
*)
echo "Generic platform — no manifest validation"
;;
esac
- name: Check update stream format
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
if [ -f "updates.xml" ]; then
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
fi
echo "updates.xml valid"
fi
;;
dolibarr)
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
;;
esac
- name: Check changelog has unreleased entry
run: |
if [ ! -f "CHANGELOG.md" ]; then
echo "::warning::No CHANGELOG.md found"
exit 0
fi
# Check for content under [Unreleased] section
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
echo "::error::CHANGELOG.md missing [Unreleased] section"
exit 1
fi
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
- name: Verify package source
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
# ── Pre-Release RC Build ─────────────────────────────────────────────────
pre-release:
name: Build RC Package
runs-on: ubuntu-latest
needs: [branch-policy, validate]
steps:
- name: Trigger RC pre-release
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 09.23.00
# BRIEF: PR gate — branch policy + code validation before merge
name: "Universal: PR Check"
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Branch Policy ──────────────────────────────────────────────────────
branch-policy:
name: Branch Policy
runs-on: ubuntu-latest
steps:
- name: Check branch merge target
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
patch/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
ALLOWED=false
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
rc)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="RC branch can only merge into 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Code Validation ────────────────────────────────────────────────────
validate:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found in source files"
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Detect platform
id: platform
run: |
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
if ! command -v php &> /dev/null; then
sudo apt-get update -qq
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
fi
- name: PHP syntax check
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
echo "PHP lint: ${ERRORS} error(s)"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Joomla JEXEC guard check
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
# Skip vendor, node_modules, and index.html stub files
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
# Check first 10 lines for JEXEC or JPATH guard
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
echo "::error file=${file}::Missing JEXEC guard: ${file}"
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
if [ "$ERRORS" -gt 0 ]; then
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "JEXEC guard: OK"
- name: Joomla directory listing protection
if: steps.platform.outputs.platform == 'joomla'
run: |
MISSING=0
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && exit 0
while IFS= read -r dir; do
if [ ! -f "${dir}/index.html" ]; then
echo "::warning::Missing index.html in ${dir} (directory listing protection)"
MISSING=$((MISSING + 1))
fi
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
if [ "$MISSING" -gt 0 ]; then
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
fi
echo "Directory protection: ${MISSING} missing (advisory)"
- name: Joomla script file and asset checks
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
[ -z "$MANIFEST" ] && exit 0
MANIFEST_DIR=$(dirname "$MANIFEST")
# Check scriptfile exists if declared
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
if [ -n "$SCRIPTFILE" ]; then
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
ERRORS=$((ERRORS + 1))
else
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
fi
fi
# Require joomla.asset.json and validate it
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$ASSET_JSON" ]; then
echo "::error::joomla.asset.json not found — Joomla asset system is required"
ERRORS=$((ERRORS + 1))
else
if command -v php &> /dev/null; then
php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
echo "::error::joomla.asset.json is not valid JSON"
ERRORS=$((ERRORS + 1))
}
fi
echo "joomla.asset.json: valid"
fi
# Validate all XML files in src/ are well-formed
XML_ERRORS=0
if command -v php &> /dev/null; then
while IFS= read -r -d '' xmlfile; do
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
XML_ERRORS=$((XML_ERRORS + 1))
fi
done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
fi
if [ "$XML_ERRORS" -gt 0 ]; then
echo "::error::${XML_ERRORS} XML file(s) are malformed"
ERRORS=$((ERRORS + 1))
else
echo "XML well-formedness: OK"
fi
[ "$ERRORS" -gt 0 ] && exit 1
echo "Joomla asset checks: OK"
- name: Validate platform manifest
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (WaaS site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
fi
for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done
# Block legacy raw/branch update server URLs on MokoGitea
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
if [ -n "$RAW_URLS" ]; then
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
echo "$RAW_URLS"
exit 1
fi
echo "Joomla manifest valid"
;;
dolibarr)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
if [ -z "$MOD_FILE" ]; then
echo "::error::No mod*.class.php found"
exit 1
fi
echo "Dolibarr module: ${MOD_FILE}"
;;
*)
echo "Generic platform — no manifest validation"
;;
esac
- name: Check update stream format
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
if [ -f "updates.xml" ]; then
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
fi
echo "updates.xml valid"
fi
;;
dolibarr)
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
;;
esac
- name: Validate Joomla language files
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
WARNINGS=0
# Require both en-GB and en-US language directories
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$LANG_ROOT" ]; then
echo "No language/ directory found — skipping"
exit 0
fi
if [ ! -d "$LANG_ROOT/en-GB" ]; then
echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
ERRORS=$((ERRORS + 1))
fi
if [ ! -d "$LANG_ROOT/en-US" ]; then
echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
ERRORS=$((ERRORS + 1))
fi
# Check that en-GB and en-US have matching .ini files
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
[ ! -f "$GB_INI" ] && continue
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
if [ ! -f "$US_INI" ]; then
echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
ERRORS=$((ERRORS + 1))
fi
done
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
[ ! -f "$US_INI" ] && continue
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
if [ ! -f "$GB_INI" ]; then
echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
ERRORS=$((ERRORS + 1))
fi
done
fi
# Find all .ini language files
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
if [ -z "$INI_FILES" ]; then
echo "No .ini language files found"
[ "$ERRORS" -gt 0 ] && exit 1
exit 0
fi
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
for FILE in $INI_FILES; do
FNAME=$(basename "$FILE")
LINENUM=0
SEEN_KEYS=""
while IFS= read -r line || [ -n "$line" ]; do
LINENUM=$((LINENUM + 1))
# Skip empty lines and comments
[ -z "$line" ] && continue
echo "$line" | grep -qE '^\s*;' && continue
echo "$line" | grep -qE '^\s*$' && continue
# Must match KEY="VALUE" format
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
ERRORS=$((ERRORS + 1))
continue
fi
# Extract key and check for duplicates
KEY=$(echo "$line" | sed 's/=.*//')
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
ERRORS=$((ERRORS + 1))
fi
SEEN_KEYS="${SEEN_KEYS}
${KEY}"
done < "$FILE"
echo " ${FILE}: checked ${LINENUM} lines"
done
# Cross-check en-GB vs en-US key consistency
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
for GB_FILE in "$GB_DIR"/*.ini; do
[ ! -f "$GB_FILE" ] && continue
FNAME=$(basename "$GB_FILE")
US_FILE="$US_DIR/$FNAME"
[ ! -f "$US_FILE" ] && continue
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
# Keys in en-GB but not en-US
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
if [ -n "$MISSING_US" ]; then
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
WARNINGS=$((WARNINGS + 1))
fi
# Keys in en-US but not en-GB
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
if [ -n "$MISSING_GB" ]; then
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
WARNINGS=$((WARNINGS + 1))
fi
done
fi
{
echo "### Language File Validation"
echo "| Metric | Count |"
echo "|---|---|"
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
echo "| Errors | ${ERRORS} |"
echo "| Warnings | ${WARNINGS} |"
} >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then
echo "::error::Language validation failed with ${ERRORS} error(s)"
exit 1
fi
echo "Language files: OK (${WARNINGS} warning(s))"
- name: Check changelog has unreleased entry
run: |
if [ ! -f "CHANGELOG.md" ]; then
echo "::warning::No CHANGELOG.md found"
exit 0
fi
# Check for content under [Unreleased] section
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
echo "::error::CHANGELOG.md missing [Unreleased] section"
exit 1
fi
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
- name: Verify package source
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
# ── Pre-Release RC Build ─────────────────────────────────────────────────
pre-release:
name: Build RC Package
runs-on: ubuntu-latest
needs: [branch-policy, validate]
steps:
- name: Trigger RC pre-release
env:
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
# ── Issue Reporter ──────────────────────────────────────────────────────
report-issues:
name: Report Issues
runs-on: ubuntu-latest
needs: [branch-policy, validate]
if: >-
always() &&
needs.validate.result == 'failure'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
sparse-checkout: automation/ci-issue-reporter.sh
sparse-checkout-cone-mode: false
- name: "File issue for PR validation failure"
env:
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
chmod +x automation/ci-issue-reporter.sh
./automation/ci-issue-reporter.sh \
--gate "PR Validation" \
--workflow "PR Check" \
--severity error \
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
-233
View File
@@ -1,233 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.01.00
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
name: "Universal: Pre-Release"
on:
pull_request:
types: [closed]
branches:
- dev
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
jobs:
build:
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.MOKOGITEA_TOKEN }}
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform-api
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform-api
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
- name: Detect platform
id: platform
run: |
php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve metadata and bump version
id: meta
run: |
STABILITY="${{ inputs.stability || 'development' }}"
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Read current version (bump already handled by push workflow)
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
[ -z "$VERSION" ] && VERSION="00.00.01"
# Strip any existing suffix from version before applying stability
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
# Verify version consistency across all files
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Update VERSION variable with suffix
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Auto-detect element via manifest_element.php
php ${MOKO_CLI}/manifest_element.php \
--path . --version "$VERSION" --stability "$STABILITY" \
--repo "${GITEA_REPO}" --github-output
# Read back element outputs
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Create release
id: release
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch dev --prerelease
- name: Build package and upload
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml -- skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push
if ! git diff --quiet updates.xml 2>/dev/null; then
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git add updates.xml
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push origin HEAD 2>&1 || echo "WARNING: push failed"
fi
- name: "Sync updates.xml to all branches"
if: steps.platform.outputs.platform == 'joomla'
run: |
CURRENT_BRANCH="${{ github.ref_name }}"
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
for BRANCH in main dev; do
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
echo "Syncing updates.xml -> ${BRANCH}"
git fetch origin "${BRANCH}" 2>/dev/null || continue
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
git checkout "${CURRENT_BRANCH}" -- updates.xml
if ! git diff --quiet updates.xml 2>/dev/null; then
git add updates.xml
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
fi
git checkout "${CURRENT_BRANCH}" 2>/dev/null
done
- name: "Delete lesser pre-release channels (cascade)"
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
php ${MOKO_CLI}/release_cascade.php \
--stability "${{ steps.meta.outputs.stability }}" \
--token "${TOKEN}" \
--api-base "${API_BASE}"
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
File diff suppressed because it is too large Load Diff
-312
View File
@@ -1,312 +0,0 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /templates/workflows/update-server.yml
# VERSION: 05.00.00
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
#
# Thin wrapper around moko-platform CLI tools.
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
#
# Joomla filters update entries by the user's "Minimum Stability" setting.
name: "Update Server"
on:
push:
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
pull_request:
types: [closed]
branches:
- 'dev'
- 'dev/**'
- 'alpha/**'
- 'beta/**'
- 'rc/**'
paths:
- 'src/**'
- 'htdocs/**'
workflow_dispatch:
inputs:
stability:
description: 'Stability tag'
required: true
default: 'development'
type: choice
options:
- development
- alpha
- beta
- rc
- stable
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
permissions:
contents: write
jobs:
update-xml:
name: Update Server
runs-on: release
if: >-
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
- name: Setup moko-platform tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
run: |
if ! command -v composer &> /dev/null; then
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
fi
# Always fetch latest CLI tools — never use stale cache from previous runs
rm -rf /tmp/moko-platform
git clone --depth 1 --branch main --quiet \
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
/tmp/moko-platform 2>/dev/null || true
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
fi
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
- name: Detect platform
id: platform
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
- name: Resolve stability and bump version
id: meta
run: |
BRANCH="${{ github.ref_name }}"
# Configure git for bot pushes
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
# Auto-bump patch version
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
# Strip any existing suffix before applying stability
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
# Determine stability from branch or manual input
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
STABILITY="${{ inputs.stability }}"
elif [[ "$BRANCH" == rc/* ]]; then
STABILITY="rc"
elif [[ "$BRANCH" == beta/* ]]; then
STABILITY="beta"
elif [[ "$BRANCH" == alpha/* ]]; then
STABILITY="alpha"
else
STABILITY="development"
fi
# Version suffix per stability stream
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
*) SUFFIX=""; TAG="stable" ;;
esac
# Propagate version with stability suffix to all manifest files
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Re-read version (now includes suffix from version_set_platform)
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
# Commit version bump if changed
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
git push
}
- name: Create release and upload package
id: package
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Create or update Gitea release
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
# Build package and upload
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
- name: Update updates.xml
if: steps.platform.outputs.platform == 'joomla'
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
if [ ! -f "updates.xml" ]; then
echo "No updates.xml — skipping"
exit 0
fi
SHA_FLAG=""
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
php ${MOKO_CLI}/updates_xml_build.php \
--path . --version "${VERSION}" --stability "${STABILITY}" \
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
${SHA_FLAG}
# Commit and push updates.xml
git add updates.xml
git diff --cached --quiet || {
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
git push
}
- name: Sync updates.xml to main
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
python3 -c "
import base64, json, urllib.request, sys
with open('updates.xml', 'rb') as f:
content = base64.b64encode(f.read()).decode()
payload = json.dumps({
'content': content,
'sha': '${FILE_SHA}',
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
'branch': 'main'
}).encode()
req = urllib.request.Request(
'${API_BASE}/contents/updates.xml',
data=payload, method='PUT',
headers={
'Authorization': 'token ${GITEA_TOKEN}',
'Content-Type': 'application/json'
})
try:
urllib.request.urlopen(req)
print('updates.xml synced to main')
except Exception as e:
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
"
fi
- name: SFTP deploy to dev server
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
env:
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
run: |
# Permission check: admin or maintain role required
ACTOR="${{ github.actor }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
case "$PERMISSION" in
admin|maintain|write) ;;
*)
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
exit 0
;;
esac
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
[ ! -d "$SOURCE_DIR" ] && exit 0
PORT="${DEV_PORT:-22}"
REMOTE="${DEV_PATH%/}"
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
if [ -n "$DEV_KEY" ]; then
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
else
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
fi
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
fi
rm -f /tmp/deploy_key /tmp/sftp-config.json
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
DISPLAY="${{ steps.meta.outputs.display_version }}"
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
+5 -31
View File
@@ -1,38 +1,12 @@
# Changelog
## [Unreleased]
## [01.12.00] --- 2026-06-04
## [01.07.00] --- 2026-05-30
## [01.11.00] --- 2026-06-04
## [01.10.00] --- 2026-06-04
All notable changes to this project will be documented in this file.
## [01.09.00] --- 2026-06-04
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Version format: `XX.YY.ZZ` (zero-padded semver).
## [01.06.00] --- 2026-05-30
## [01.04.00] - 2026-05-30
### Added
- Local Video hero mode with Joomla Media Manager file picker (`localVideoFile` param)
- Install script (`script.php`) creates `images/heroes/` folder on install/update
## [01.03.00] - 2026-05-30
### Added
- Configurable card fade-in delay with slide-up animation (`cardDelay` param, 0-5000ms) (#39)
- Video mute/unmute toggle button (`showMuteToggle` param) -- supports YouTube, Vimeo, and native video (#40)
## [01.02.00] - 2026-05-30
### Fixed
- WebAsset registration: added `#style`/`#script` suffixes to preset dependencies (was causing `UnsatisfiedDependencyException`)
- Asset URI resolution: use `extension/filename` pattern instead of `extension/folder/filename` (Joomla auto-inserts `css/`/`js/` folders)
- iframe video cover: split CSS into `<video>` (`object-fit: cover`) and `<iframe>` (oversize + centre-crop) rules since `object-fit` doesn't apply to iframes
- Card link styling: exclude `.btn` elements from `color: inherit` so buttons retain their own styles
### Added
- Module title renders inside the hero card as `<h2>`, respecting Joomla's Show Title toggle
- IntersectionObserver pauses/resumes videos when the hero scrolls out of/into the viewport (YouTube, Vimeo, and native `<video>`)
- YouTube embeds include `enablejsapi=1` for postMessage playback control
## [01.08.00] --- 2026-06-04
+30 -9
View File
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code when working with this repository.
## Project Overview
**MokoJoomHero** -- A Joomla Module designed to provide a random image from a folder with content on top as a Hero.
**MokoJoomHero** -- Random hero image slideshow/video with content overlay for Joomla
| Field | Value |
|---|---|
@@ -32,20 +32,41 @@ composer install # Install PHP dependencies
## Architecture
This is a Joomla extension. Key directories:
- `src/` -- extension source (deployed to Joomla)
- `src/*.xml` -- manifest file (version, files, params)
- `src/src/` or `src/services/` -- PHP classes
- `src/language/` -- translation strings
- `src/media/` -- CSS/JS/images
This is a Joomla **package** extension (`pkg_mokojoomhero`) containing two sub-extensions:
### mod_mokojoomhero (Site Module)
- Random hero image slideshow or background video with content overlay
- Supports image folders, YouTube, Vimeo, local video, solid colour, gradient
- Configurable overlay, text alignment, card animation, parallax, content animations
- Works independently — no plugin dependency required
### plg_system_mokojoomhero (System Plugin)
- License check — free tier (no key required), pro tier warns if download key missing
- Controlled by `LICENSE_TYPE` constant ('free' or 'pro') in the Extension class
- Auto-enabled on package install via `pkg_script.php`
- Namespace: `Joomla\Plugin\System\MokoJoomHero`
### Key files
- `src/pkg_mokojoomhero.xml` — package manifest
- `src/pkg_script.php` — auto-enables system plugin on install
- `src/packages/mod_mokojoomhero/` — hero module source
- `src/packages/plg_system_mokojoomhero/` — system plugin source
- `updates.xml` — Joomla update server (includes legacy module entries for migration)
## Rules
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
- **Attribution**: use `Authored-by: Moko Consulting` in commits
- **Branch strategy**: develop on `dev`, merge to `main` for release
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates)
- **Minification**: handled at build time (CI)
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
## Coding Standards
- PHP 8.1+ minimum
- Joomla 5/6 DI container pattern: `services/provider.php` → Extension class
- Legacy stub `.php` file required for plugin loader but empty
- `SubscriberInterface` for event subscription (not `on*` method naming)
- SPDX license headers on all PHP files
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP:
INGROUP: Project.Documentation
REPO:
VERSION: 01.07.00
VERSION: 01.12.00
PATH: ./CODE_OF_CONDUCT.md
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
-->
+141 -108
View File
@@ -1,128 +1,161 @@
<!--
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# Contributing to Moko Consulting Projects
This file is part of a Moko Consulting project.
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, 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 (./LICENSE).
# FILE INFORMATION
DEFGROUP: {{DEFGROUP}}
INGROUP: Project.Documentation
REPO: https://github.com/mokoconsulting-tech/MokoJoomHero
VERSION: 01.07.00
PATH: ./CONTRIBUTING.md
BRIEF: How to contribute; branch strategy, commit conventions, PR workflow, and release pipeline
-->
# Contributing
Thank you for your interest in contributing to **MokoJoomHero**!
This repository is governed by **[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards)** — the authoritative source of coding standards, workflows, and policies for all Moko Consulting repositories.
## Branch Strategy
| Branch | Purpose | Deploys To |
|--------|---------|------------|
| `main` | Bleeding edge — all development merges here | CI only |
| `dev/XX.YY.ZZ` | Feature development | Dev server (version: "development") |
| `version/XX.YY` | Stable frozen snapshot | Demo + RS servers |
### Development Workflow
## Branching Workflow
```
1. Create branch: git checkout -b dev/XX.YY.ZZ/my-feature
2. Develop + test (dev server auto-deploys on push)
3. Open PR → main (squash merge only)
4. Auto-release (version branch + tag + GitHub Release created automatically)
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
```
### Branch Naming
### Step by step
| Prefix | Use |
|--------|-----|
| `dev/XX.YY.ZZ` | Feature development (e.g., `dev/02.00.00/add-extrafields`) |
| `version/XX.YY` | Stable release (auto-created, never manually pushed) |
| `chore/` | Automated sync branches (managed by MokoStandards) |
1. **Create a feature branch** from `dev`:
```bash
git checkout dev && git pull
git checkout -b feature/my-change
```
> **Never use** `feature/`, `hotfix/`, or `release/` prefixes — they are not part of the MokoStandards branch strategy.
2. **Work and commit** on your feature branch. Push to origin.
## Commit Conventions
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
Use [conventional commits](https://www.conventionalcommits.org/):
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
- This automatically renames the source branch to `rc` (release candidate)
- An RC pre-release is built and uploaded
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
- When the draft PR is created, the branch is renamed to `rc`
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
7. **Merging to main** triggers the stable release pipeline:
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
- Stability suffix stripped (clean version)
- Gitea release created with ZIP/tar.gz packages
- `updates.xml` updated (Joomla extensions)
- `dev` branch recreated from `main`
### Branch summary
| Branch | Purpose | Created by |
|--------|---------|-----------|
| `feature/*` | New features and fixes | Developer |
| `dev` | Integration branch | Auto-recreated after release |
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
| `rc` | Release candidate | Auto-renamed on draft PR to main |
| `main` | Stable releases | Protected, merge only |
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
### Protected branches
| Branch | Direct push | Merge via |
|--------|------------|-----------|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
| `feature/*` | Open | N/A (source branch) |
## Version Policy
### Format
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
- **XX** — Major version (breaking changes)
- **YY** — Minor version (new features, bumped on release to main)
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
### Stability suffixes
Each branch appends a suffix to indicate stability:
| Branch | Suffix | Example |
|--------|--------|---------|
| `main` | (none) | `02.09.00` |
| `dev` | `-dev` | `02.09.01-dev` |
| `feature/*` | `-dev` | `02.09.01-dev` |
| `alpha` | `-alpha` | `02.09.01-alpha` |
| `beta` | `-beta` | `02.09.01-beta` |
| `rc` | `-rc` | `02.09.01-rc` |
### Auto version bump
On every push to `dev`, `feature/*`, or `patch/*`:
1. Patch version incremented
2. Stability suffix `-dev` applied
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
4. Commit created with `[skip ci]` to avoid loops
### Release version flow
Version bumps happen at specific release events:
| Event | Bump | Example |
|-------|------|---------|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
### Release stream copies
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
### Version files
The version tools update all files containing version stamps:
- `.mokogitea/manifest.xml` (canonical source)
- Joomla XML manifests (`<version>` tag)
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
- `package.json`, `pyproject.toml`
- Any text file with a `VERSION: XX.YY.ZZ` label
Files synced from other repos (with a `# REPO:` header) are not touched.
## Code Standards
- **PHP**: PSR-12, tabs for indentation
- **Copyright**: all files must include the Moko Consulting copyright header
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
## Commit Messages
Use conventional commit format:
```
feat(scope): add new extrafield for invoice tracking
fix(sql): correct column type in llx_mytable
docs(readme): update installation instructions
chore(deps): bump enterprise library to 04.02.30
type(scope): short description
Optional body with context.
Authored-by: Moko Consulting
```
**Valid types:** `feat` | `fix` | `docs` | `chore` | `ci` | `refactor` | `style` | `test` | `perf` | `revert` | `build`
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
## Pull Request Workflow
Special flags in commit messages:
- `[skip ci]` — skip all CI workflows
- `[skip bump]` — skip auto version bump only
1. **Branch** from `main` using `dev/XX.YY.ZZ/description` format
2. **Bump** the patch version in `README.md` before opening the PR
3. **Title** must be a valid conventional commit subject line
4. **Target** `main` — squash merge only (merge commits are disabled)
5. **CI checks** must pass before merge
## Reporting Issues
### What Happens on Merge
When your PR is merged to `main`, these workflows run automatically:
1. **sync-version-on-merge** — auto-bumps patch version, propagates to all file headers
2. **auto-release** — creates `version/XX.YY` branch, git tag, and GitHub Release
3. **deploy-demo / deploy-rs** — deploys to demo and RS servers (if `src/**` changed)
## Coding Standards
All contributions must follow [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards):
| Standard | Reference |
|----------|-----------|
| Coding Style | [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) |
| File Headers | [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) |
| Branching | [branch-release-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branch-release-strategy.md) |
| Merge Strategy | [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) |
| Scripting | [scripting-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) |
| Build & Release | [build-release.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/workflows/build-release.md) |
## PR Checklist
- [ ] Branch named `dev/XX.YY.ZZ/description`
- [ ] Patch version bumped in `README.md`
- [ ] Conventional commit format for PR title
- [ ] All new files have FILE INFORMATION headers
- [ ] `declare(strict_types=1)` in all PHP files
- [ ] PHPDoc on all public methods
- [ ] Tests pass
- [ ] CHANGELOG.md updated
- [ ] No secrets, tokens, or credentials committed
## Custom Workflows
Place repo-specific workflows in `.github/workflows/custom/` — they are **never overwritten or deleted** by MokoStandards sync:
```
.github/workflows/
├── deploy-dev.yml ← Synced from MokoStandards
├── auto-release.yml ← Synced from MokoStandards
└── custom/ ← Your custom workflows (safe)
└── my-custom-ci.yml
```
## License
By contributing, you agree that your contributions will be licensed under the [GPL-3.0-or-later](LICENSE) license.
Use the repository's issue tracker with the appropriate template.
---
*This file is synced from [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Do not edit directly — changes will be overwritten on the next sync.*
*Moko Consulting <hello@mokoconsulting.tech>*
+34 -100
View File
@@ -13,7 +13,7 @@
# Extension Configuration
EXTENSION_NAME := mokojoomhero
EXTENSION_TYPE := module
EXTENSION_TYPE := package
# Options: module, plugin, component, package, template
EXTENSION_VERSION := 1.0.0
@@ -26,7 +26,7 @@ PLUGIN_GROUP := system
# Options: system, content, user, authentication, etc.
# Directories
SRC_DIR := .
SRC_DIR := src
BUILD_DIR := build
DIST_DIR := dist
DOCS_DIR := docs
@@ -155,62 +155,30 @@ clean: ## Clean build artifacts
@echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)"
.PHONY: build
build: clean validate ## Build extension package
@echo "$(COLOR_BLUE)Building Joomla extension package...$(COLOR_RESET)"
@mkdir -p $(DIST_DIR) $(BUILD_DIR)
# Determine package prefix based on extension type
@case "$(EXTENSION_TYPE)" in \
module) \
PACKAGE_PREFIX="mod_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
plugin) \
PACKAGE_PREFIX="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
component) \
PACKAGE_PREFIX="com_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
package) \
PACKAGE_PREFIX="pkg_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
template) \
PACKAGE_PREFIX="tpl_$(EXTENSION_NAME)"; \
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
;; \
*) \
echo "$(COLOR_RED)✗ Unknown extension type: $(EXTENSION_TYPE)$(COLOR_RESET)"; \
exit 1; \
;; \
esac; \
\
mkdir -p "$$BUILD_TARGET"; \
\
echo "Building $$PACKAGE_PREFIX..."; \
\
rsync -av --progress \
--exclude='$(BUILD_DIR)' \
--exclude='$(DIST_DIR)' \
--exclude='.git*' \
--exclude='vendor/' \
--exclude='node_modules/' \
--exclude='tests/' \
--exclude='Makefile' \
--exclude='composer.json' \
--exclude='composer.lock' \
--exclude='package.json' \
--exclude='package-lock.json' \
--exclude='phpunit.xml' \
--exclude='*.md' \
--exclude='.editorconfig' \
. "$$BUILD_TARGET/"; \
\
cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip" "$${PACKAGE_PREFIX}"; \
\
echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
build: clean ## Build extension package
@echo "$(COLOR_BLUE)Building Joomla package extension...$(COLOR_RESET)"
@mkdir -p $(DIST_DIR) $(BUILD_DIR)/packages
@# --- Build each sub-extension as a separate ZIP ---
@for EXT_DIR in $(SRC_DIR)/packages/*/; do \
EXT_NAME=$$(basename "$$EXT_DIR"); \
[ "$$EXT_NAME" = "index.html" ] && continue; \
echo " Packaging $$EXT_NAME..."; \
cd "$$EXT_DIR" && $(ZIP) -r "$(CURDIR)/$(BUILD_DIR)/packages/$${EXT_NAME}.zip" . \
-x "*.git*" -x "*/index.html" 2>/dev/null; \
cd "$(CURDIR)"; \
done
@# --- Build the outer package ZIP ---
@echo " Assembling pkg_$(EXTENSION_NAME)..."
@cp $(SRC_DIR)/pkg_mokojoomhero.xml $(BUILD_DIR)/pkg_mokojoomhero.xml
@cp $(SRC_DIR)/pkg_script.php $(BUILD_DIR)/pkg_script.php
@cd $(BUILD_DIR) && $(ZIP) -r "$(CURDIR)/$(DIST_DIR)/pkg_$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip" \
pkg_mokojoomhero.xml pkg_script.php packages/
@echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/pkg_$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
@echo " Contents:"
@unzip -l "$(DIST_DIR)/pkg_$(EXTENSION_NAME)-$(EXTENSION_VERSION).zip" | tail -n +4 | head -20
.PHONY: package
package: build ## Alias for build
@@ -325,49 +293,15 @@ security-check: ## Run security checks on dependencies
$(NPM) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
fi
.PHONY: deploy
deploy: ## Deploy to a Joomla site via SSH (usage: make deploy HOST=user@host WEBROOT=/path/to/joomla)
@if [ -z "$(HOST)" ] || [ -z "$(WEBROOT)" ]; then \
echo "$(COLOR_RED)✗ Usage: make deploy HOST=user@host WEBROOT=/path/to/joomla [KEY=~/.ssh/id_rsa]$(COLOR_RESET)"; \
exit 1; \
.PHONY: minify
minify: ## Minify CSS/JS assets
@echo "Minifying assets..."
@MOKO_PLATFORM=$$(echo ../moko-platform $$HOME/moko-platform /opt/moko-platform | tr ' ' '\n' | while read p; do [ -d "$$p" ] && echo "$$p" && break; done); \
if [ -n "$$MOKO_PLATFORM" ] && [ -f "$$MOKO_PLATFORM/build/minify.js" ]; then \
node "$$MOKO_PLATFORM/build/minify.js" $(SRC_DIR); \
else \
echo "No minify script found"; \
fi
@SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"; \
if [ -n "$(KEY)" ]; then SSH_OPTS="$$SSH_OPTS -i $(KEY)"; fi; \
echo "$(COLOR_BLUE)Deploying mod_$(EXTENSION_NAME) to $(HOST):$(WEBROOT)...$(COLOR_RESET)"; \
ssh $$SSH_OPTS $(HOST) "\
W=$(WEBROOT) && \
cp -r \$$W/modules/mod_$(EXTENSION_NAME)/language/en-US/* /dev/null 2>&1; \
true" && \
for f in src/mod_mokojoomhero.php src/mod_mokojoomhero.xml src/script.php; do \
scp $$SSH_OPTS $$f $(HOST):$(WEBROOT)/modules/mod_$(EXTENSION_NAME)/$$(basename $$f); \
done && \
scp -r $$SSH_OPTS src/tmpl/* $(HOST):$(WEBROOT)/modules/mod_$(EXTENSION_NAME)/tmpl/ && \
scp -r $$SSH_OPTS src/language/* $(HOST):$(WEBROOT)/modules/mod_$(EXTENSION_NAME)/language/ && \
scp $$SSH_OPTS src/media/joomla.asset.json $(HOST):$(WEBROOT)/media/mod_$(EXTENSION_NAME)/ && \
scp -r $$SSH_OPTS src/media/css/* $(HOST):$(WEBROOT)/media/mod_$(EXTENSION_NAME)/css/ && \
scp -r $$SSH_OPTS src/media/js/* $(HOST):$(WEBROOT)/media/mod_$(EXTENSION_NAME)/js/ && \
ssh $$SSH_OPTS $(HOST) "\
W=$(WEBROOT) && \
mkdir -p \$$W/images/heroes && \
for lang in en-US en-GB; do \
for ini in mod_mokojoomhero.ini mod_mokojoomhero.sys.ini; do \
src=\$$W/modules/mod_$(EXTENSION_NAME)/language/\$$lang/\$$ini; \
if [ -f \$$src ]; then \
cp \$$src \$$W/administrator/language/\$$lang/\$$ini 2>/dev/null; \
cp \$$src \$$W/language/\$$lang/\$$ini 2>/dev/null; \
fi; \
done; \
done && \
echo 'OK'" && \
echo "$(COLOR_GREEN)✓ Deployed to $(HOST)$(COLOR_RESET)"
.PHONY: deploy-all
deploy-all: ## Deploy to all configured sites (requires SITES_FILE or inline)
@echo "$(COLOR_BLUE)Deploying to all sites...$(COLOR_RESET)"
@echo "$(COLOR_YELLOW)Usage: Create a sites.conf with HOST:WEBROOT per line, then:$(COLOR_RESET)"
@echo " while IFS=: read -r host webroot; do"
@echo " make deploy HOST=\$$host WEBROOT=\$$webroot KEY=path/to/key"
@echo " done < sites.conf"
.PHONY: all
all: install-deps validate test build ## Run complete build pipeline
+1 -1
View File
@@ -7,7 +7,7 @@
# FILE INFORMATION
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
FILE: ./README.md
VERSION: 01.07.00
VERSION: 01.12.00
BRIEF: MokoJoomHero - Joomla Module
-->
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
INGROUP: [PROJECT_NAME].Documentation
REPO: [REPOSITORY_URL]
PATH: /SECURITY.md
VERSION: 01.07.00
VERSION: 01.12.00
BRIEF: Security vulnerability reporting and handling policy
-->
+237
View File
@@ -0,0 +1,237 @@
#!/usr/bin/env bash
# ============================================================================
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Automation.CI
# INGROUP: moko-platform.Automation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
# PATH: /automation/ci-issue-reporter.sh
# VERSION: 09.23.00
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
# Deduplicates by searching open issues with the "ci-auto" label
# whose title matches the gate. If a matching issue exists, a comment
# is appended instead of opening a duplicate.
# ============================================================================
set -euo pipefail
# ── Defaults ────────────────────────────────────────────────────────────────
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
REPO="${GITHUB_REPOSITORY:-}"
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
LABEL_NAME="ci-auto"
LABEL_COLOR="#e11d48"
GATE=""
DETAILS=""
SEVERITY="error"
WORKFLOW=""
# ── Parse arguments ─────────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
Required:
--gate CI gate name (e.g. "Code Quality", "Self-Health")
--details Human-readable failure description
Optional:
--severity "error" (default) or "warning"
--workflow Workflow name for the issue title
--repo owner/repo (default: \$GITHUB_REPOSITORY)
--run-url URL to the CI run (auto-detected from env)
--token Gitea API token (default: \$GITEA_TOKEN)
--url Gitea base URL (default: \$GITEA_URL)
EOF
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--gate) GATE="$2"; shift 2 ;;
--details) DETAILS="$2"; shift 2 ;;
--severity) SEVERITY="$2"; shift 2 ;;
--workflow) WORKFLOW="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--run-url) RUN_URL="$2"; shift 2 ;;
--token) GITEA_TOKEN="$2"; shift 2 ;;
--url) GITEA_URL="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
done
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
API="${GITEA_URL}/api/v1/repos/${REPO}"
# ── Build title ─────────────────────────────────────────────────────────────
if [[ -n "$WORKFLOW" ]]; then
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
else
TITLE="[CI] ${GATE} failed"
fi
# ── Ensure label exists ─────────────────────────────────────────────────────
ensure_label() {
local exists
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null || echo "000")
if [[ "$exists" == "200" ]]; then
# Check if label already exists
local found
found=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
if [[ -z "$found" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/labels" \
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
> /dev/null 2>&1 || true
fi
fi
}
# ── Search for existing open issue ──────────────────────────────────────────
find_existing_issue() {
# URL-encode the gate name for the query
local query
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
local response
response=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
2>/dev/null || echo "[]")
# Extract the first matching issue number
echo "$response" \
| grep -oP '"number":\s*\K[0-9]+' \
| head -1
}
# ── Build issue body ────────────────────────────────────────────────────────
build_body() {
local severity_badge
if [[ "$SEVERITY" == "error" ]]; then
severity_badge="**Severity:** Error"
else
severity_badge="**Severity:** Warning"
fi
cat <<BODY
## CI Gate Failure: ${GATE}
${severity_badge}
**Workflow:** ${WORKFLOW:-unknown}
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
### Details
${DETAILS}
### Resolution
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
---
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
BODY
}
# ── Build comment body (for existing issues) ────────────────────────────────
build_comment() {
cat <<COMMENT
### CI failure recurrence
**Branch:** ${GITHUB_REF_NAME:-unknown}
**Commit:** \`${GITHUB_SHA:0:8}\`
**Run:** [View CI run](${RUN_URL})
${DETAILS}
COMMENT
}
# ── Main ────────────────────────────────────────────────────────────────────
ensure_label
EXISTING=$(find_existing_issue)
if [[ -n "$EXISTING" ]]; then
# Append comment to existing issue
COMMENT_BODY=$(build_comment)
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
import sys, json
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${EXISTING}/comments" \
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
if [[ "$HTTP" == "201" ]]; then
echo "Commented on existing issue #${EXISTING}"
else
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
fi
else
# Create new issue
ISSUE_BODY=$(build_body)
ISSUE_JSON=$(python3 -c "
import sys, json
body = sys.stdin.read()
print(json.dumps({
'title': sys.argv[1],
'body': body,
'labels': []
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
# Create the issue
RESPONSE=$(curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues" \
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
if [[ -n "$ISSUE_NUM" ]]; then
# Apply label (separate call — more reliable across Gitea versions)
LABEL_ID=$(curl -sf \
-H "Authorization: token ${GITEA_TOKEN}" \
"${API}/labels" 2>/dev/null \
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
| head -1 || true)
if [[ -n "$LABEL_ID" ]]; then
curl -sf -X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
"${API}/issues/${ISSUE_NUM}/labels" \
-d "{\"labels\":[${LABEL_ID}]}" \
> /dev/null 2>&1 || true
fi
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
else
echo "WARNING: Failed to create issue"
echo "Response: ${RESPONSE}"
fi
fi
+1
View File
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
-16
View File
@@ -1,16 +0,0 @@
# Docs Index: /templates/repos/joomla/module/src
## Purpose
This index provides navigation to documentation within this folder.
## Metadata
- **Document Type:** index
- **Auto-generated:** This file is automatically generated by rebuild_indexes.py
## Revision History
| Change | Notes | Author |
| --- | --- | --- |
| Automated update | Generated by documentation index automation | rebuild_indexes.py |
-71
View File
@@ -1,71 +0,0 @@
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
; SPDX-License-Identifier: GPL-3.0-or-later
;
; FILE INFORMATION
; DEFGROUP: MokoJoomHero.Module.Language
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-GB/mod_mokojoomhero.ini
; VERSION: 01.07.00
; BRIEF: Language strings for MokoJoomHero module (frontend + admin form fields)
MOD_MOKOJOOMHERO_NO_CONTENT="Add content to this module to display it over the hero image."
; Content fieldset
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
; Hero mode
MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode"
MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), or a local video file."
MOD_MOKOJOOMHERO_MODE_IMAGES="Images"
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
; Image settings
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)."
MOD_MOKOJOOMHERO_IMAGE_COUNT_LABEL="Number of Images"
MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slideshow (15)."
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
; Video settings (embedded)
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
; Local video settings
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
; Card delay
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
; Mute toggle
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)."
; Hero height
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60%% of screen)."
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
; Overlay fieldset
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay &amp; Text"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Colour"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background colour of the overlay on top of the hero image."
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Colour"
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Colour of the text displayed over the hero image."
; Alignment options
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
MOD_MOKOJOOMHERO_ALIGN_CENTER="Centre"
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
@@ -1,72 +0,0 @@
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
; SPDX-License-Identifier: GPL-3.0-or-later
;
; FILE INFORMATION
; DEFGROUP: MokoJoomHero.Module.Language
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-GB/mod_mokojoomhero.sys.ini
; VERSION: 01.07.00
; BRIEF: System language strings — used in admin Extension Manager and Module Manager
MOD_MOKOJOOMHERO="Module - MokoJoomHero"
MOD_MOKOJOOMHERO_DESCRIPTION="Displays a random hero image slideshow or background video with content overlaid. Designed for MokoOnyx template. By Moko Consulting."
; Content fieldset
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
; Hero mode
MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode"
MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), or a local video file."
MOD_MOKOJOOMHERO_MODE_IMAGES="Images"
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
; Image settings
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)."
MOD_MOKOJOOMHERO_IMAGE_COUNT_LABEL="Number of Images"
MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slideshow (15)."
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
; Video settings (embedded)
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
; Local video settings
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
; Card delay
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
; Mute toggle
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)."
; Hero height
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60%% of screen)."
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
; Overlay fieldset
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay &amp; Text"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Colour"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background colour of the overlay on top of the hero image."
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Colour"
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Colour of the text displayed over the hero image."
; Alignment options
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
MOD_MOKOJOOMHERO_ALIGN_CENTER="Centre"
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
-71
View File
@@ -1,71 +0,0 @@
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
; SPDX-License-Identifier: GPL-3.0-or-later
;
; FILE INFORMATION
; DEFGROUP: MokoJoomHero.Module.Language
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-US/mod_mokojoomhero.ini
; VERSION: 01.07.00
; BRIEF: Language strings for MokoJoomHero module (en-US, frontend + admin form fields)
MOD_MOKOJOOMHERO_NO_CONTENT="Add content to this module to display it over the hero image."
; Content fieldset
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
; Hero mode
MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode"
MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), or a local video file."
MOD_MOKOJOOMHERO_MODE_IMAGES="Images"
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
; Image settings
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)."
MOD_MOKOJOOMHERO_IMAGE_COUNT_LABEL="Number of Images"
MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slideshow (1-5)."
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
; Video settings (embedded)
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
; Local video settings
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
; Hero height
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60% of screen)."
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
; Card delay
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
; Mute toggle
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)."
; Overlay fieldset
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay &amp; Text"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Color"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background color of the overlay on top of the hero image."
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Color"
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Color of the text displayed over the hero image."
; Alignment options
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
MOD_MOKOJOOMHERO_ALIGN_CENTER="Center"
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
@@ -1,72 +0,0 @@
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
; SPDX-License-Identifier: GPL-3.0-or-later
;
; FILE INFORMATION
; DEFGROUP: MokoJoomHero.Module.Language
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-US/mod_mokojoomhero.sys.ini
; VERSION: 01.07.00
; BRIEF: System language strings — used in admin Extension Manager and Module Manager (en-US)
MOD_MOKOJOOMHERO="Module - MokoJoomHero"
MOD_MOKOJOOMHERO_DESCRIPTION="Displays a random hero image slideshow or background video with content overlaid. Designed for MokoOnyx template. By Moko Consulting."
; Content fieldset
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
; Hero mode
MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode"
MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), or a local video file."
MOD_MOKOJOOMHERO_MODE_IMAGES="Images"
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
; Image settings
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)."
MOD_MOKOJOOMHERO_IMAGE_COUNT_LABEL="Number of Images"
MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slideshow (1-5)."
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
; Video settings (embedded)
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
; Local video settings
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
; Card delay
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
; Mute toggle
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)."
; Hero height
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60% of screen)."
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
; Overlay fieldset
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay &amp; Text"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Color"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background color of the overlay on top of the hero image."
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Color"
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Color of the text displayed over the hero image."
; Alignment options
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
MOD_MOKOJOOMHERO_ALIGN_CENTER="Center"
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
-193
View File
@@ -1,193 +0,0 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoJoomHero.Module.Assets
* INGROUP: MokoJoomHero.Module
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
* PATH: /src/css/template.css
* VERSION: 01.07.00
* BRIEF: Hero module stylesheet — slideshow, video background, overlay
*/
/* ============================================================
Hero container
============================================================ */
.mokojoomhero {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
/* ============================================================
Image slides
============================================================ */
.mokojoomhero__slide {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: 0;
transition: opacity 1s ease;
}
.mokojoomhero__slide--active {
opacity: 1;
}
/* ============================================================
Video background
============================================================ */
/* Native <video> elements: object-fit works directly */
video.mokojoomhero__video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
border: 0;
pointer-events: none;
}
/* Embedded <iframe> (YouTube/Vimeo): object-fit doesn't apply to iframes,
so we oversize the iframe and centre-crop via the parent's overflow:hidden */
iframe.mokojoomhero__video {
position: absolute;
top: 50%;
left: 50%;
width: 100vw;
height: 56.25vw; /* 16:9 aspect ratio */
min-height: 100%;
min-width: 177.78vh; /* 100 × 16/9 — ensures cover in portrait viewports */
transform: translate(-50%, -50%);
border: 0;
pointer-events: none;
}
/* ============================================================
Overlay
============================================================ */
.mokojoomhero__overlay {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 2rem;
}
/* ============================================================
Content
============================================================ */
.mokojoomhero__content {
max-width: 900px;
width: 100%;
}
.mokojoomhero__content h1,
.mokojoomhero__content h2,
.mokojoomhero__content h3 {
margin-top: 0;
color: inherit;
}
.mokojoomhero__content p:last-child {
margin-bottom: 0;
}
/* ============================================================
Card
============================================================ */
.mokojoomhero__card {
background: rgba(255, 255, 255, 0.95);
color: #333;
border-radius: 8px;
padding: 2rem 2.5rem;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
max-width: 700px;
margin: 0 auto;
}
.mokojoomhero__card h1,
.mokojoomhero__card h2,
.mokojoomhero__card h3 {
color: #222;
margin-top: 0;
}
.mokojoomhero__card a:not(.btn) {
color: inherit;
text-decoration: underline;
}
/* ============================================================
Card fade-in delay
============================================================ */
.mokojoomhero__card[data-card-delay] {
opacity: 0;
animation: mokojoomhero-fadein 0.6s ease forwards;
}
@keyframes mokojoomhero-fadein {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ============================================================
Mute toggle
============================================================ */
.mokojoomhero__mute-toggle {
position: absolute;
bottom: 1rem;
right: 1rem;
z-index: 2;
background: rgba(0, 0, 0, 0.5);
color: #fff;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 1.2rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.mokojoomhero__mute-toggle:hover {
background: rgba(0, 0, 0, 0.7);
}
/* ============================================================
Responsive
============================================================ */
@media (max-width: 768px) {
.mokojoomhero {
height: auto !important;
}
.mokojoomhero__video,
.mokojoomhero__slide {
display: none;
}
.mokojoomhero__overlay {
padding: 1rem;
background-color: transparent !important;
}
.mokojoomhero__content {
font-size: 0.9rem;
}
.mokojoomhero__card {
padding: 1.5rem;
}
}
-118
View File
@@ -1,118 +0,0 @@
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoJoomHero.Module.Assets
* INGROUP: MokoJoomHero.Module
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
* PATH: /src/js/template.js
* VERSION: 01.07.00
* BRIEF: Hero module JavaScript — image slideshow crossfade
*/
'use strict';
document.addEventListener('DOMContentLoaded', function () {
// Skip slideshow on mobile — video/images are hidden by CSS
if (window.matchMedia('(max-width: 768px)').matches) {
return;
}
// ── Image slideshow ──
document.querySelectorAll('.mokojoomhero[data-slides]').forEach(function (hero) {
var slides = hero.querySelectorAll('.mokojoomhero__slide');
var interval = parseInt(hero.dataset.interval, 10) || 5000;
var current = 0;
if (slides.length < 2) {
return;
}
setInterval(function () {
slides[current].classList.remove('mokojoomhero__slide--active');
slides[current].setAttribute('aria-hidden', 'true');
current = (current + 1) % slides.length;
slides[current].classList.add('mokojoomhero__slide--active');
slides[current].setAttribute('aria-hidden', 'false');
}, interval);
});
// ── Pause/resume videos when out of viewport ──
if (!('IntersectionObserver' in window)) {
return;
}
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
var hero = entry.target;
var video = hero.querySelector('video.mokojoomhero__video');
var iframe = hero.querySelector('iframe.mokojoomhero__video');
if (entry.isIntersecting) {
// Resume
if (video) {
video.play();
}
if (iframe) {
var src = iframe.src || '';
if (src.indexOf('youtube') !== -1) {
iframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*');
} else if (src.indexOf('vimeo') !== -1) {
iframe.contentWindow.postMessage('{"method":"play"}', '*');
}
}
} else {
// Pause
if (video) {
video.pause();
}
if (iframe) {
var src = iframe.src || '';
if (src.indexOf('youtube') !== -1) {
iframe.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
} else if (src.indexOf('vimeo') !== -1) {
iframe.contentWindow.postMessage('{"method":"pause"}', '*');
}
}
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.mokojoomhero').forEach(function (hero) {
observer.observe(hero);
});
// ── Mute/unmute toggle ──
document.querySelectorAll('.mokojoomhero__mute-toggle').forEach(function (btn) {
var hero = btn.closest('.mokojoomhero');
var video = hero.querySelector('video.mokojoomhero__video');
var iframe = hero.querySelector('iframe.mokojoomhero__video');
var icon = btn.querySelector('.mokojoomhero__mute-icon');
btn.addEventListener('click', function () {
var muted = btn.getAttribute('data-muted') === 'true';
if (video) {
video.muted = !muted;
}
if (iframe) {
var src = iframe.src || '';
if (src.indexOf('youtube') !== -1) {
var func = muted ? 'unMute' : 'mute';
iframe.contentWindow.postMessage('{"event":"command","func":"' + func + '","args":""}', '*');
} else if (src.indexOf('vimeo') !== -1) {
var vol = muted ? 1 : 0;
iframe.contentWindow.postMessage('{"method":"setVolume","value":' + vol + '}', '*');
}
}
btn.setAttribute('data-muted', muted ? 'false' : 'true');
btn.setAttribute('aria-label', muted ? 'Mute video' : 'Unmute video');
icon.textContent = muted ? '\u{1F50A}' : '\u{1F507}';
});
});
});
-94
View File
@@ -1,94 +0,0 @@
<?php
/**
* @package Joomla.Site
* @subpackage mod_mokojoomhero
*
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\CMS\Uri\Uri;
/** @var \Joomla\CMS\Application\SiteApplication $app */
/** @var \stdClass $module */
/** @var \Joomla\Registry\Registry $params */
// Load module assets via Web Asset Manager (registry in media/mod_mokojoomhero/)
$wa = $app->getDocument()->getWebAssetManager();
$wa->getRegistry()->addExtensionRegistryFile('mod_mokojoomhero');
$wa->usePreset('mod_mokojoomhero');
// Module parameters
$heroMode = $params->get('heroMode', 'images');
$imageFolder = $params->get('imageFolder', 'images/heroes');
$imageCount = (int) $params->get('imageCount', 5);
$slideInterval = (int) $params->get('slideInterval', 5000);
$videoFile = $params->get('videoFile', '');
$heroHeight = $params->get('heroHeight', '60vh');
$overlayColor = $params->get('overlayColor', '#000000');
$overlayOpacity = (float) $params->get('overlayOpacity', 0.5);
$textAlign = $params->get('textAlign', 'center');
$textColor = $params->get('textColor', '#ffffff');
$heroContent = $params->get('heroContent', '');
$showCard = (bool) $params->get('showCard', 1);
$cardDelay = (int) $params->get('cardDelay', 0);
$showMuteToggle = (bool) $params->get('showMuteToggle', 0);
$localVideoFile = $params->get('localVideoFile', '');
// Collect hero images
$heroImages = [];
if ($heroMode === 'images') {
$folderPath = JPATH_ROOT . '/' . ltrim($imageFolder, '/');
if (is_dir($folderPath)) {
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg'];
$all = [];
foreach (new DirectoryIterator($folderPath) as $file) {
if ($file->isFile() && in_array(strtolower($file->getExtension()), $allowed, true)) {
$all[] = $file->getFilename();
}
}
if ($all) {
shuffle($all);
$picked = array_slice($all, 0, min($imageCount, 5));
foreach ($picked as $filename) {
$heroImages[] = Uri::root() . $imageFolder . '/' . $filename;
}
}
}
}
// Build video URL — smartly detect YouTube, Vimeo, or local/direct file
$videoUrl = '';
$youtubeId = '';
$vimeoId = '';
if ($heroMode === 'localvideo' && $localVideoFile) {
$videoUrl = Uri::root() . ltrim($localVideoFile, '/');
} elseif ($heroMode === 'video' && $videoFile) {
// YouTube: watch, embed, shorts, youtu.be, with optional timestamps/params
if (preg_match('/(?:youtube\.com\/(?:watch\?.*v=|embed\/|shorts\/|v\/)|youtu\.be\/)([\w-]{11})/', $videoFile, $m)) {
$youtubeId = $m[1];
// Vimeo: vimeo.com/123456 or player.vimeo.com/video/123456
} elseif (preg_match('/vimeo\.com\/(?:video\/)?(\d+)/', $videoFile, $m)) {
$vimeoId = $m[1];
} else {
// Direct URL or local file path
$videoUrl = (strpos($videoFile, '://') !== false)
? $videoFile
: Uri::root() . ltrim($videoFile, '/');
}
}
// Module content from the editor (overlay text)
$content = $module->content ?? '';
require ModuleHelper::getLayoutPath('mod_mokojoomhero', $params->get('layout', 'default'));
-215
View File
@@ -1,215 +0,0 @@
<?xml version="1.0" encoding="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: MokoJoomHero.Module
INGROUP: MokoJoomHero
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
PATH: /src/mod_mokojoomhero.xml
VERSION: 01.00.20
BRIEF: Joomla module manifest — random hero image with content overlay
-->
<extension type="module" client="site" method="upgrade">
<name>Module - MokoJoomHero</name>
<creationDate>2026-05</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<version>01.07.00</version>
<description>Displays a random hero image slideshow or background video with content overlaid. Designed for MokoOnyx template. By Moko Consulting.</description>
<scriptfile>script.php</scriptfile>
<files>
<filename module="mod_mokojoomhero">mod_mokojoomhero.php</filename>
<filename>mod_mokojoomhero.xml</filename>
<filename>script.php</filename>
<folder>tmpl</folder>
<folder>language</folder>
</files>
<media destination="mod_mokojoomhero" folder="media">
<filename>joomla.asset.json</filename>
<folder>css</folder>
<folder>js</folder>
</media>
<languages folder="language">
<language tag="en-GB">en-GB/mod_mokojoomhero.ini</language>
<language tag="en-GB">en-GB/mod_mokojoomhero.sys.ini</language>
<language tag="en-US">en-US/mod_mokojoomhero.ini</language>
<language tag="en-US">en-US/mod_mokojoomhero.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="heroMode"
type="list"
label="MOD_MOKOJOOMHERO_MODE_LABEL"
description="MOD_MOKOJOOMHERO_MODE_DESC"
default="images"
>
<option value="images">MOD_MOKOJOOMHERO_MODE_IMAGES</option>
<option value="video">MOD_MOKOJOOMHERO_MODE_VIDEO</option>
<option value="localvideo">MOD_MOKOJOOMHERO_MODE_LOCALVIDEO</option>
</field>
<field
name="imageFolder"
type="text"
label="MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL"
description="MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC"
default="images/heroes"
filter="path"
showon="heroMode:images"
/>
<field
name="imageCount"
type="number"
label="MOD_MOKOJOOMHERO_IMAGE_COUNT_LABEL"
description="MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC"
default="5"
min="1"
max="5"
showon="heroMode:images"
/>
<field
name="slideInterval"
type="number"
label="MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL"
description="MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC"
default="5000"
min="1000"
step="500"
showon="heroMode:images"
/>
<field
name="videoFile"
type="text"
label="MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL"
description="MOD_MOKOJOOMHERO_VIDEO_FILE_DESC"
filter="string"
showon="heroMode:video"
/>
<field
name="localVideoFile"
type="media"
label="MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL"
description="MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC"
types="videos"
showon="heroMode:localvideo"
/>
<field
name="heroHeight"
type="text"
label="MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL"
description="MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC"
hint="MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT"
default="60vh"
filter="string"
/>
<field
name="showMuteToggle"
type="radio"
layout="joomla.form.field.radio.switcher"
label="MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL"
description="MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC"
default="0"
showon="heroMode:video,localvideo"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
</fieldset>
<fieldset name="content"
label="MOD_MOKOJOOMHERO_FIELDSET_CONTENT"
>
<field
name="heroContent"
type="editor"
label="MOD_MOKOJOOMHERO_CONTENT_LABEL"
description="MOD_MOKOJOOMHERO_CONTENT_DESC"
filter="safehtml"
buttons="true"
hide="readmore,pagebreak"
/>
<field
name="showCard"
type="radio"
layout="joomla.form.field.radio.switcher"
label="MOD_MOKOJOOMHERO_SHOW_CARD_LABEL"
description="MOD_MOKOJOOMHERO_SHOW_CARD_DESC"
default="1"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="cardDelay"
type="number"
label="MOD_MOKOJOOMHERO_CARD_DELAY_LABEL"
description="MOD_MOKOJOOMHERO_CARD_DELAY_DESC"
default="0"
min="0"
max="5000"
step="250"
showon="showCard:1"
/>
</fieldset>
<fieldset name="advanced"
label="MOD_MOKOJOOMHERO_FIELDSET_OVERLAY"
>
<field
name="overlayColor"
type="color"
label="MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL"
description="MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC"
default="#000000"
/>
<field
name="overlayOpacity"
type="range"
label="MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL"
description="MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC"
default="0.5"
min="0"
max="1"
step="0.1"
/>
<field
name="textAlign"
type="list"
label="MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL"
description="MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC"
default="center"
>
<option value="left">MOD_MOKOJOOMHERO_ALIGN_LEFT</option>
<option value="center">MOD_MOKOJOOMHERO_ALIGN_CENTER</option>
<option value="right">MOD_MOKOJOOMHERO_ALIGN_RIGHT</option>
</field>
<field
name="textColor"
type="color"
label="MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL"
description="MOD_MOKOJOOMHERO_TEXT_COLOR_DESC"
default="#ffffff"
/>
</fieldset>
</fields>
</config>
<updateservers>
<server type="extension" priority="1" name="MokoJoomHero Updates">
https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/updates.xml
</server>
</updateservers>
</extension>
+1
View File
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
+1
View File
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -0,0 +1,172 @@
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
; SPDX-License-Identifier: GPL-3.0-or-later
;
; FILE INFORMATION
; DEFGROUP: MokoJoomHero.Module.Language
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-GB/mod_mokojoomhero.ini
; VERSION: 01.12.00
; BRIEF: Language strings for MokoJoomHero module (frontend + admin form fields)
MOD_MOKOJOOMHERO_NO_CONTENT="Add content to this module to display it over the hero image."
; Content fieldset
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL="Content Source"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC="Choose whether to enter content manually or pull from a Joomla article."
MOD_MOKOJOOMHERO_SOURCE_MANUAL="Manual Editor"
MOD_MOKOJOOMHERO_SOURCE_ARTICLE="Joomla Article"
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
MOD_MOKOJOOMHERO_ARTICLE_LABEL="Article"
MOD_MOKOJOOMHERO_ARTICLE_DESC="Select a published article to use as the hero content. The article introtext (or fulltext) is displayed."
MOD_MOKOJOOMHERO_ARTICLE_SELECT="- Select Article -"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL="Use Article Title"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC="Replace the module title with the selected article's title."
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
; Hero mode
MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode"
MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), a local video file, a solid colour, or a gradient."
MOD_MOKOJOOMHERO_MODE_IMAGES="Images"
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
MOD_MOKOJOOMHERO_MODE_COLOR="Solid Colour"
MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient"
; Transition type
MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type"
MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides."
MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade"
MOD_MOKOJOOMHERO_FADE_SLIDE="Slide"
MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black"
MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)"
; Image settings
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)."
MOD_MOKOJOOMHERO_IMAGE_COUNT_LABEL="Number of Images"
MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slideshow (15)."
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
; Per-slide content
MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL="Slide Content"
MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC="Define individual slides with unique images and content. When populated, this overrides the random image folder. Leave empty to use the folder-based slideshow."
MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL="Image"
MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL="Heading"
MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL="Body Text"
MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL="Link URL"
MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL="Link Text"
; Video settings (embedded)
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
; Local video settings
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
; Content animation
MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL="Content Animation"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC="Entrance animation for the overlay content when the hero scrolls into view."
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL="Animation Delay (ms)"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC="Delay before the content animation starts."
MOD_MOKOJOOMHERO_ANIM_NONE="None"
MOD_MOKOJOOMHERO_ANIM_FADE_IN="Fade In"
MOD_MOKOJOOMHERO_ANIM_SLIDE_UP="Slide Up"
MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT="Slide from Right"
MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT="Slide from Left"
; Card delay
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
; Parallax
MOD_MOKOJOOMHERO_PARALLAX_LABEL="Parallax Effect"
MOD_MOKOJOOMHERO_PARALLAX_DESC="Background moves at a slower rate than page content on scroll, creating a depth effect."
MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL="Parallax Speed"
MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC="How much the background moves relative to scroll (0.1 = subtle, 0.9 = dramatic)."
; A/B testing
MOD_MOKOJOOMHERO_FIELDSET_AB="A/B Testing"
MOD_MOKOJOOMHERO_AB_ENABLED_LABEL="Enable A/B Testing"
MOD_MOKOJOOMHERO_AB_ENABLED_DESC="Randomly show different content variations to visitors. Assignment is sticky per session."
MOD_MOKOJOOMHERO_AB_VARIATIONS_LABEL="Variations"
MOD_MOKOJOOMHERO_AB_VARIATIONS_DESC="Define content variations with relative weights. Higher weight = higher chance of being shown."
MOD_MOKOJOOMHERO_AB_VAR_LABEL="Label"
MOD_MOKOJOOMHERO_AB_VAR_CONTENT="Content"
MOD_MOKOJOOMHERO_AB_VAR_WEIGHT="Weight"
; Scheduling
MOD_MOKOJOOMHERO_FIELDSET_SCHEDULING="Scheduling"
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_LABEL="Enable Scheduling"
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_DESC="Only display the hero during a specific date/time range. Uses the site timezone."
MOD_MOKOJOOMHERO_SCHEDULE_START_LABEL="Start Date/Time"
MOD_MOKOJOOMHERO_SCHEDULE_START_DESC="The hero will not display before this date and time. Leave empty for no start restriction."
MOD_MOKOJOOMHERO_SCHEDULE_END_LABEL="End Date/Time"
MOD_MOKOJOOMHERO_SCHEDULE_END_DESC="The hero will not display after this date and time. Leave empty for no end restriction."
; Video poster
MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image"
MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads. Prevents a blank hero on slow connections."
; Scroll indicator
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator"
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll."
; Mute toggle
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)."
; Solid colour background
MOD_MOKOJOOMHERO_BG_COLOR_LABEL="Background Colour"
MOD_MOKOJOOMHERO_BG_COLOR_DESC="Solid background colour for the hero section."
; Gradient background
MOD_MOKOJOOMHERO_GRADIENT_START_LABEL="Gradient Start Colour"
MOD_MOKOJOOMHERO_GRADIENT_START_DESC="Starting colour of the gradient."
MOD_MOKOJOOMHERO_GRADIENT_END_LABEL="Gradient End Colour"
MOD_MOKOJOOMHERO_GRADIENT_END_DESC="Ending colour of the gradient."
MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL="Gradient Angle"
MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC="Direction of the gradient in degrees (0 = bottom to top, 90 = left to right, 135 = diagonal)."
; Hero height
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60%% of screen)."
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
; Hero height (mobile)
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height. Uses the same units as Hero Height."
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)"
; Overlay fieldset
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay &amp; Text"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction."
MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Colour"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background colour of the overlay on top of the hero image."
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment"
MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero."
MOD_MOKOJOOMHERO_VALIGN_TOP="Top"
MOD_MOKOJOOMHERO_VALIGN_CENTER="Centre"
MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom"
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Colour"
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Colour of the text displayed over the hero image."
; Horizontal alignment options
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
MOD_MOKOJOOMHERO_ALIGN_CENTER="Centre"
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
@@ -0,0 +1,173 @@
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
; SPDX-License-Identifier: GPL-3.0-or-later
;
; FILE INFORMATION
; DEFGROUP: MokoJoomHero.Module.Language
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-GB/mod_mokojoomhero.sys.ini
; VERSION: 01.12.00
; BRIEF: System language strings — used in admin Extension Manager and Module Manager
MOD_MOKOJOOMHERO="Module - MokoJoomHero"
MOD_MOKOJOOMHERO_DESCRIPTION="Displays a random hero image slideshow or background video with content overlaid. Designed for MokoOnyx template. By Moko Consulting."
; Content fieldset
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL="Content Source"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC="Choose whether to enter content manually or pull from a Joomla article."
MOD_MOKOJOOMHERO_SOURCE_MANUAL="Manual Editor"
MOD_MOKOJOOMHERO_SOURCE_ARTICLE="Joomla Article"
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
MOD_MOKOJOOMHERO_ARTICLE_LABEL="Article"
MOD_MOKOJOOMHERO_ARTICLE_DESC="Select a published article to use as the hero content."
MOD_MOKOJOOMHERO_ARTICLE_SELECT="- Select Article -"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL="Use Article Title"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC="Replace the module title with the selected article's title."
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
; Hero mode
MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode"
MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), a local video file, a solid colour, or a gradient."
MOD_MOKOJOOMHERO_MODE_IMAGES="Images"
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
MOD_MOKOJOOMHERO_MODE_COLOR="Solid Colour"
MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient"
; Transition type
MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type"
MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides."
MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade"
MOD_MOKOJOOMHERO_FADE_SLIDE="Slide"
MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black"
MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)"
; Image settings
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)."
MOD_MOKOJOOMHERO_IMAGE_COUNT_LABEL="Number of Images"
MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slideshow (15)."
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
; Per-slide content
MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL="Slide Content"
MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC="Define individual slides with unique images and content."
MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL="Image"
MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL="Heading"
MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL="Body Text"
MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL="Link URL"
MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL="Link Text"
; Video settings (embedded)
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
; Local video settings
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
; Content animation
MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL="Content Animation"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC="Entrance animation for the overlay content when the hero scrolls into view."
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL="Animation Delay (ms)"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC="Delay before the content animation starts."
MOD_MOKOJOOMHERO_ANIM_NONE="None"
MOD_MOKOJOOMHERO_ANIM_FADE_IN="Fade In"
MOD_MOKOJOOMHERO_ANIM_SLIDE_UP="Slide Up"
MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT="Slide from Right"
MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT="Slide from Left"
; Card delay
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
; Parallax
MOD_MOKOJOOMHERO_PARALLAX_LABEL="Parallax Effect"
MOD_MOKOJOOMHERO_PARALLAX_DESC="Background moves at a slower rate than page content on scroll."
MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL="Parallax Speed"
MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC="How much the background moves relative to scroll (0.1 = subtle, 0.9 = dramatic)."
; A/B testing
MOD_MOKOJOOMHERO_FIELDSET_AB="A/B Testing"
MOD_MOKOJOOMHERO_AB_ENABLED_LABEL="Enable A/B Testing"
MOD_MOKOJOOMHERO_AB_ENABLED_DESC="Randomly show different content variations to visitors."
MOD_MOKOJOOMHERO_AB_VARIATIONS_LABEL="Variations"
MOD_MOKOJOOMHERO_AB_VARIATIONS_DESC="Define content variations with relative weights."
MOD_MOKOJOOMHERO_AB_VAR_LABEL="Label"
MOD_MOKOJOOMHERO_AB_VAR_CONTENT="Content"
MOD_MOKOJOOMHERO_AB_VAR_WEIGHT="Weight"
; Scheduling
MOD_MOKOJOOMHERO_FIELDSET_SCHEDULING="Scheduling"
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_LABEL="Enable Scheduling"
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_DESC="Only display the hero during a specific date/time range."
MOD_MOKOJOOMHERO_SCHEDULE_START_LABEL="Start Date/Time"
MOD_MOKOJOOMHERO_SCHEDULE_START_DESC="The hero will not display before this date and time."
MOD_MOKOJOOMHERO_SCHEDULE_END_LABEL="End Date/Time"
MOD_MOKOJOOMHERO_SCHEDULE_END_DESC="The hero will not display after this date and time."
; Video poster
MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image"
MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads."
; Scroll indicator
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator"
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll."
; Mute toggle
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)."
; Solid colour background
MOD_MOKOJOOMHERO_BG_COLOR_LABEL="Background Colour"
MOD_MOKOJOOMHERO_BG_COLOR_DESC="Solid background colour for the hero section."
; Gradient background
MOD_MOKOJOOMHERO_GRADIENT_START_LABEL="Gradient Start Colour"
MOD_MOKOJOOMHERO_GRADIENT_START_DESC="Starting colour of the gradient."
MOD_MOKOJOOMHERO_GRADIENT_END_LABEL="Gradient End Colour"
MOD_MOKOJOOMHERO_GRADIENT_END_DESC="Ending colour of the gradient."
MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL="Gradient Angle"
MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC="Direction of the gradient in degrees (0 = bottom to top, 90 = left to right, 135 = diagonal)."
; Hero height
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60%% of screen)."
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
; Hero height (mobile)
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height."
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)"
; Overlay fieldset
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay &amp; Text"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction."
MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Colour"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background colour of the overlay on top of the hero image."
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment"
MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero."
MOD_MOKOJOOMHERO_VALIGN_TOP="Top"
MOD_MOKOJOOMHERO_VALIGN_CENTER="Centre"
MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom"
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Colour"
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Colour of the text displayed over the hero image."
; Horizontal alignment options
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
MOD_MOKOJOOMHERO_ALIGN_CENTER="Centre"
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -0,0 +1,172 @@
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
; SPDX-License-Identifier: GPL-3.0-or-later
;
; FILE INFORMATION
; DEFGROUP: MokoJoomHero.Module.Language
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-US/mod_mokojoomhero.ini
; VERSION: 01.12.00
; BRIEF: Language strings for MokoJoomHero module (en-US, frontend + admin form fields)
MOD_MOKOJOOMHERO_NO_CONTENT="Add content to this module to display it over the hero image."
; Content fieldset
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL="Content Source"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC="Choose whether to enter content manually or pull from a Joomla article."
MOD_MOKOJOOMHERO_SOURCE_MANUAL="Manual Editor"
MOD_MOKOJOOMHERO_SOURCE_ARTICLE="Joomla Article"
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
MOD_MOKOJOOMHERO_ARTICLE_LABEL="Article"
MOD_MOKOJOOMHERO_ARTICLE_DESC="Select a published article to use as the hero content. The article introtext (or fulltext) is displayed."
MOD_MOKOJOOMHERO_ARTICLE_SELECT="- Select Article -"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL="Use Article Title"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC="Replace the module title with the selected article's title."
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
; Hero mode
MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode"
MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), a local video file, a solid color, or a gradient."
MOD_MOKOJOOMHERO_MODE_IMAGES="Images"
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
MOD_MOKOJOOMHERO_MODE_COLOR="Solid Color"
MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient"
; Transition type
MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type"
MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides."
MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade"
MOD_MOKOJOOMHERO_FADE_SLIDE="Slide"
MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black"
MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)"
; Image settings
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)."
MOD_MOKOJOOMHERO_IMAGE_COUNT_LABEL="Number of Images"
MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slideshow (1-5)."
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
; Per-slide content
MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL="Slide Content"
MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC="Define individual slides with unique images and content. When populated, this overrides the random image folder. Leave empty to use the folder-based slideshow."
MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL="Image"
MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL="Heading"
MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL="Body Text"
MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL="Link URL"
MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL="Link Text"
; Video settings (embedded)
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
; Local video settings
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
; Solid color background
MOD_MOKOJOOMHERO_BG_COLOR_LABEL="Background Color"
MOD_MOKOJOOMHERO_BG_COLOR_DESC="Solid background color for the hero section."
; Gradient background
MOD_MOKOJOOMHERO_GRADIENT_START_LABEL="Gradient Start Color"
MOD_MOKOJOOMHERO_GRADIENT_START_DESC="Starting color of the gradient."
MOD_MOKOJOOMHERO_GRADIENT_END_LABEL="Gradient End Color"
MOD_MOKOJOOMHERO_GRADIENT_END_DESC="Ending color of the gradient."
MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL="Gradient Angle"
MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC="Direction of the gradient in degrees (0 = bottom to top, 90 = left to right, 135 = diagonal)."
; Hero height
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60% of screen)."
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
; Card delay
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
; Content animation
MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL="Content Animation"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC="Entrance animation for the overlay content when the hero scrolls into view."
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL="Animation Delay (ms)"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC="Delay before the content animation starts."
MOD_MOKOJOOMHERO_ANIM_NONE="None"
MOD_MOKOJOOMHERO_ANIM_FADE_IN="Fade In"
MOD_MOKOJOOMHERO_ANIM_SLIDE_UP="Slide Up"
MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT="Slide from Right"
MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT="Slide from Left"
; Parallax
MOD_MOKOJOOMHERO_PARALLAX_LABEL="Parallax Effect"
MOD_MOKOJOOMHERO_PARALLAX_DESC="Background moves at a slower rate than page content on scroll, creating a depth effect."
MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL="Parallax Speed"
MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC="How much the background moves relative to scroll (0.1 = subtle, 0.9 = dramatic)."
; A/B testing
MOD_MOKOJOOMHERO_FIELDSET_AB="A/B Testing"
MOD_MOKOJOOMHERO_AB_ENABLED_LABEL="Enable A/B Testing"
MOD_MOKOJOOMHERO_AB_ENABLED_DESC="Randomly show different content variations to visitors. Assignment is sticky per session."
MOD_MOKOJOOMHERO_AB_VARIATIONS_LABEL="Variations"
MOD_MOKOJOOMHERO_AB_VARIATIONS_DESC="Define content variations with relative weights. Higher weight = higher chance of being shown."
MOD_MOKOJOOMHERO_AB_VAR_LABEL="Label"
MOD_MOKOJOOMHERO_AB_VAR_CONTENT="Content"
MOD_MOKOJOOMHERO_AB_VAR_WEIGHT="Weight"
; Scheduling
MOD_MOKOJOOMHERO_FIELDSET_SCHEDULING="Scheduling"
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_LABEL="Enable Scheduling"
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_DESC="Only display the hero during a specific date/time range. Uses the site timezone."
MOD_MOKOJOOMHERO_SCHEDULE_START_LABEL="Start Date/Time"
MOD_MOKOJOOMHERO_SCHEDULE_START_DESC="The hero will not display before this date and time. Leave empty for no start restriction."
MOD_MOKOJOOMHERO_SCHEDULE_END_LABEL="End Date/Time"
MOD_MOKOJOOMHERO_SCHEDULE_END_DESC="The hero will not display after this date and time. Leave empty for no end restriction."
; Video poster
MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image"
MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads. Prevents a blank hero on slow connections."
; Scroll indicator
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator"
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll."
; Mute toggle
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)."
; Hero height (mobile)
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height. Uses the same units as Hero Height."
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)"
; Overlay fieldset
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay &amp; Text"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction."
MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Color"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background color of the overlay on top of the hero image."
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment"
MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero."
MOD_MOKOJOOMHERO_VALIGN_TOP="Top"
MOD_MOKOJOOMHERO_VALIGN_CENTER="Center"
MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom"
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Color"
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Color of the text displayed over the hero image."
; Horizontal alignment options
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
MOD_MOKOJOOMHERO_ALIGN_CENTER="Center"
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
@@ -0,0 +1,173 @@
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
; SPDX-License-Identifier: GPL-3.0-or-later
;
; FILE INFORMATION
; DEFGROUP: MokoJoomHero.Module.Language
; INGROUP: MokoJoomHero.Module
; REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
; PATH: /src/language/en-US/mod_mokojoomhero.sys.ini
; VERSION: 01.12.00
; BRIEF: System language strings — used in admin Extension Manager and Module Manager (en-US)
MOD_MOKOJOOMHERO="Module - MokoJoomHero"
MOD_MOKOJOOMHERO_DESCRIPTION="Displays a random hero image slideshow or background video with content overlaid. Designed for MokoOnyx template. By Moko Consulting."
; Content fieldset
MOD_MOKOJOOMHERO_FIELDSET_CONTENT="Hero Content"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL="Content Source"
MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC="Choose whether to enter content manually or pull from a Joomla article."
MOD_MOKOJOOMHERO_SOURCE_MANUAL="Manual Editor"
MOD_MOKOJOOMHERO_SOURCE_ARTICLE="Joomla Article"
MOD_MOKOJOOMHERO_CONTENT_LABEL="Content"
MOD_MOKOJOOMHERO_CONTENT_DESC="HTML content displayed on the hero. Use the editor to add headings, text, buttons, or any HTML."
MOD_MOKOJOOMHERO_ARTICLE_LABEL="Article"
MOD_MOKOJOOMHERO_ARTICLE_DESC="Select a published article to use as the hero content."
MOD_MOKOJOOMHERO_ARTICLE_SELECT="- Select Article -"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL="Use Article Title"
MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC="Replace the module title with the selected article's title."
MOD_MOKOJOOMHERO_SHOW_CARD_LABEL="Show Card"
MOD_MOKOJOOMHERO_SHOW_CARD_DESC="Wrap the content in a card with a white background and shadow."
; Hero mode
MOD_MOKOJOOMHERO_MODE_LABEL="Hero Mode"
MOD_MOKOJOOMHERO_MODE_DESC="Choose between a slideshow of images, an embedded video (YouTube/Vimeo), a local video file, a solid color, or a gradient."
MOD_MOKOJOOMHERO_MODE_IMAGES="Images"
MOD_MOKOJOOMHERO_MODE_VIDEO="Video (YouTube/Vimeo)"
MOD_MOKOJOOMHERO_MODE_LOCALVIDEO="Local Video"
MOD_MOKOJOOMHERO_MODE_COLOR="Solid Color"
MOD_MOKOJOOMHERO_MODE_GRADIENT="Gradient"
; Transition type
MOD_MOKOJOOMHERO_FADE_TYPE_LABEL="Transition Type"
MOD_MOKOJOOMHERO_FADE_TYPE_DESC="How images transition between slides."
MOD_MOKOJOOMHERO_FADE_CROSSFADE="Crossfade"
MOD_MOKOJOOMHERO_FADE_SLIDE="Slide"
MOD_MOKOJOOMHERO_FADE_BLACK="Fade to Black"
MOD_MOKOJOOMHERO_FADE_ZOOM="Zoom (Ken Burns)"
; Image settings
MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL="Image Folder"
MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC="Path to folder containing hero images, relative to Joomla root (e.g. images/heroes)."
MOD_MOKOJOOMHERO_IMAGE_COUNT_LABEL="Number of Images"
MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC="How many random images to include in the slideshow (1-5)."
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL="Slide Interval (ms)"
MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC="Time between slides in milliseconds (e.g. 5000 = 5 seconds)."
; Per-slide content
MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL="Slide Content"
MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC="Define individual slides with unique images and content."
MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL="Image"
MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL="Heading"
MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL="Body Text"
MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL="Link URL"
MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL="Link Text"
; Video settings (embedded)
MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL="Video URL"
MOD_MOKOJOOMHERO_VIDEO_FILE_DESC="YouTube or Vimeo URL. The module auto-detects the source."
; Local video settings
MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL="Video File"
MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC="Select a video file from the Media Manager (mp4, webm, ogg)."
; Content animation
MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL="Content Animation"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC="Entrance animation for the overlay content when the hero scrolls into view."
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL="Animation Delay (ms)"
MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC="Delay before the content animation starts."
MOD_MOKOJOOMHERO_ANIM_NONE="None"
MOD_MOKOJOOMHERO_ANIM_FADE_IN="Fade In"
MOD_MOKOJOOMHERO_ANIM_SLIDE_UP="Slide Up"
MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT="Slide from Right"
MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT="Slide from Left"
; Card delay
MOD_MOKOJOOMHERO_CARD_DELAY_LABEL="Card Fade-in Delay (ms)"
MOD_MOKOJOOMHERO_CARD_DELAY_DESC="Delay in milliseconds before the content card fades in. Set to 0 for no delay."
; Parallax
MOD_MOKOJOOMHERO_PARALLAX_LABEL="Parallax Effect"
MOD_MOKOJOOMHERO_PARALLAX_DESC="Background moves at a slower rate than page content on scroll."
MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL="Parallax Speed"
MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC="How much the background moves relative to scroll (0.1 = subtle, 0.9 = dramatic)."
; A/B testing
MOD_MOKOJOOMHERO_FIELDSET_AB="A/B Testing"
MOD_MOKOJOOMHERO_AB_ENABLED_LABEL="Enable A/B Testing"
MOD_MOKOJOOMHERO_AB_ENABLED_DESC="Randomly show different content variations to visitors."
MOD_MOKOJOOMHERO_AB_VARIATIONS_LABEL="Variations"
MOD_MOKOJOOMHERO_AB_VARIATIONS_DESC="Define content variations with relative weights."
MOD_MOKOJOOMHERO_AB_VAR_LABEL="Label"
MOD_MOKOJOOMHERO_AB_VAR_CONTENT="Content"
MOD_MOKOJOOMHERO_AB_VAR_WEIGHT="Weight"
; Scheduling
MOD_MOKOJOOMHERO_FIELDSET_SCHEDULING="Scheduling"
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_LABEL="Enable Scheduling"
MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_DESC="Only display the hero during a specific date/time range."
MOD_MOKOJOOMHERO_SCHEDULE_START_LABEL="Start Date/Time"
MOD_MOKOJOOMHERO_SCHEDULE_START_DESC="The hero will not display before this date and time."
MOD_MOKOJOOMHERO_SCHEDULE_END_LABEL="End Date/Time"
MOD_MOKOJOOMHERO_SCHEDULE_END_DESC="The hero will not display after this date and time."
; Video poster
MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL="Video Poster Image"
MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC="Fallback image displayed while the video loads."
; Scroll indicator
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL="Show Scroll Indicator"
MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC="Show an animated down-arrow at the bottom of the hero prompting users to scroll."
; Mute toggle
MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL="Show Mute Toggle"
MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC="Show a mute/unmute button on the hero video. Videos always start muted (required for autoplay)."
; Solid color background
MOD_MOKOJOOMHERO_BG_COLOR_LABEL="Background Color"
MOD_MOKOJOOMHERO_BG_COLOR_DESC="Solid background color for the hero section."
; Gradient background
MOD_MOKOJOOMHERO_GRADIENT_START_LABEL="Gradient Start Color"
MOD_MOKOJOOMHERO_GRADIENT_START_DESC="Starting color of the gradient."
MOD_MOKOJOOMHERO_GRADIENT_END_LABEL="Gradient End Color"
MOD_MOKOJOOMHERO_GRADIENT_END_DESC="Ending color of the gradient."
MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL="Gradient Angle"
MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC="Direction of the gradient in degrees (0 = bottom to top, 90 = left to right, 135 = diagonal)."
; Hero height
MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL="Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC="Height of the hero section. Use px for fixed pixels (e.g. 400px) or vh for viewport height (e.g. 60vh for 60% of screen)."
MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT="e.g. 60vh or 400px"
; Hero height (mobile)
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL="Mobile Hero Height"
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC="Height of the hero on mobile devices. Leave empty for auto height."
MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT="e.g. 40vh or 300px (empty = auto)"
; Overlay fieldset
MOD_MOKOJOOMHERO_FIELDSET_OVERLAY="Overlay &amp; Text"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL="Overlay Type"
MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC="How the overlay is applied. Solid fills evenly; gradient fades from transparent to opaque in the chosen direction."
MOD_MOKOJOOMHERO_OVERLAY_SOLID="Solid"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM="Gradient (dark at bottom)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP="Gradient (dark at top)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT="Gradient (dark at left)"
MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT="Gradient (dark at right)"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL="Overlay Color"
MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC="Background color of the overlay on top of the hero image."
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL="Overlay Opacity"
MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC="Transparency of the overlay (0 = fully transparent, 1 = fully opaque)."
MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL="Text Alignment"
MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC="Horizontal alignment of the overlay text."
MOD_MOKOJOOMHERO_VALIGN_LABEL="Vertical Alignment"
MOD_MOKOJOOMHERO_VALIGN_DESC="Vertical position of the content within the hero."
MOD_MOKOJOOMHERO_VALIGN_TOP="Top"
MOD_MOKOJOOMHERO_VALIGN_CENTER="Center"
MOD_MOKOJOOMHERO_VALIGN_BOTTOM="Bottom"
MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL="Text Color"
MOD_MOKOJOOMHERO_TEXT_COLOR_DESC="Color of the text displayed over the hero image."
; Horizontal alignment options
MOD_MOKOJOOMHERO_ALIGN_LEFT="Left"
MOD_MOKOJOOMHERO_ALIGN_CENTER="Center"
MOD_MOKOJOOMHERO_ALIGN_RIGHT="Right"
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -0,0 +1,382 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoJoomHero.Module.Assets
* INGROUP: MokoJoomHero.Module
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
* PATH: /src/packages/mod_mokojoomhero/media/css/mod_mokojoomhero.css
* VERSION: 01.12.00
* BRIEF: Hero module stylesheet — slideshow, video, colour/gradient, overlay, card, mute toggle, responsive
*/
/* ============================================================
Hero container
============================================================ */
.mokojoomhero {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
/* ============================================================
Solid colour / gradient background
============================================================ */
.mokojoomhero__color {
position: absolute;
inset: 0;
}
/* ============================================================
Image slides — base
============================================================ */
.mokojoomhero__slide {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: 0;
}
.mokojoomhero__slide--active {
opacity: 1;
}
/* ── Crossfade (default) ── */
.mokojoomhero[data-transition="crossfade"] .mokojoomhero__slide {
transition: opacity 1s ease;
}
/* ── Slide ── */
.mokojoomhero[data-transition="slide"] .mokojoomhero__slide {
opacity: 1;
transform: translateX(100%);
transition: transform 0.8s ease;
}
.mokojoomhero[data-transition="slide"] .mokojoomhero__slide--active {
transform: translateX(0);
}
.mokojoomhero[data-transition="slide"] .mokojoomhero__slide--exit {
transform: translateX(-100%);
}
/* ── Fade to black ── */
.mokojoomhero[data-transition="fade-black"] .mokojoomhero__slide {
transition: opacity 0.6s ease;
}
/* ── Zoom (Ken Burns) ── */
.mokojoomhero[data-transition="zoom"] .mokojoomhero__slide {
transition: opacity 1s ease;
}
.mokojoomhero[data-transition="zoom"] .mokojoomhero__slide--active {
animation: mokojoomhero-zoom 8s ease forwards;
}
@keyframes mokojoomhero-zoom {
from { transform: scale(1); }
to { transform: scale(1.08); }
}
/* ============================================================
Video background
============================================================ */
/* Native <video> elements: object-fit works directly */
video.mokojoomhero__video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
border: 0;
pointer-events: none;
}
/* Embedded <iframe> (YouTube/Vimeo): object-fit doesn't apply to iframes,
so we oversize the iframe and centre-crop via the parent's overflow:hidden */
iframe.mokojoomhero__video {
position: absolute;
top: 50%;
left: 50%;
width: 100vw;
height: 56.25vw; /* 16:9 aspect ratio */
min-height: 100%;
min-width: 177.78vh; /* 100 × 16/9 — ensures cover in portrait viewports */
transform: translate(-50%, -50%);
border: 0;
pointer-events: none;
}
/* ============================================================
Overlay
============================================================ */
.mokojoomhero__overlay {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
width: 100%;
height: 100%;
padding: 2rem;
}
/* ============================================================
Content
============================================================ */
.mokojoomhero__content {
max-width: 900px;
width: 100%;
}
.mokojoomhero__content h1,
.mokojoomhero__content h2,
.mokojoomhero__content h3 {
margin-top: 0;
color: inherit;
}
.mokojoomhero__content p:last-child {
margin-bottom: 0;
}
/* ============================================================
Card
============================================================ */
.mokojoomhero__card {
background: rgba(255, 255, 255, 0.95);
color: #333;
border-radius: 8px;
padding: 2rem 2.5rem;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
max-width: 700px;
margin: 0 auto;
}
.mokojoomhero__card h1,
.mokojoomhero__card h2,
.mokojoomhero__card h3 {
color: #222;
margin-top: 0;
}
.mokojoomhero__card a:not(.btn) {
color: inherit;
text-decoration: underline;
}
/* ============================================================
Card fade-in delay
============================================================ */
.mokojoomhero__card[data-card-delay] {
opacity: 0;
animation: mokojoomhero-fadein 0.6s ease forwards;
}
@keyframes mokojoomhero-fadein {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ============================================================
Mute toggle
============================================================ */
.mokojoomhero__mute-toggle {
position: absolute;
bottom: 1rem;
right: 1rem;
z-index: 2;
background: rgba(0, 0, 0, 0.5);
color: #fff;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 1.2rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.mokojoomhero__mute-toggle:hover {
background: rgba(0, 0, 0, 0.7);
}
/* ============================================================
Video poster image
============================================================ */
.mokojoomhero__poster {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
/* ============================================================
Scroll-down indicator
============================================================ */
.mokojoomhero__scroll-indicator {
position: absolute;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%);
z-index: 2;
background: none;
border: none;
color: #fff;
cursor: pointer;
padding: 0;
opacity: 0.8;
transition: opacity 0.3s;
animation: mokojoomhero-bounce 2s infinite;
}
.mokojoomhero__scroll-indicator:hover {
opacity: 1;
}
.mokojoomhero__scroll-indicator--hidden {
display: none;
}
@keyframes mokojoomhero-bounce {
0%, 20%, 50%, 80%, 100% { transform: translateX(-50%) translateY(0); }
40% { transform: translateX(-50%) translateY(-8px); }
60% { transform: translateX(-50%) translateY(-4px); }
}
/* ============================================================
Content entrance animations
============================================================ */
.mokojoomhero__content[class*="mokojoomhero__content--anim-"] {
opacity: 0;
}
.mokojoomhero__content--anim-fade-in.mokojoomhero__content--visible {
animation: mokojoomhero-anim-fade-in 0.8s ease forwards;
}
.mokojoomhero__content--anim-slide-up.mokojoomhero__content--visible {
animation: mokojoomhero-anim-slide-up 0.8s ease forwards;
}
.mokojoomhero__content--anim-slide-left.mokojoomhero__content--visible {
animation: mokojoomhero-anim-slide-left 0.8s ease forwards;
}
.mokojoomhero__content--anim-slide-right.mokojoomhero__content--visible {
animation: mokojoomhero-anim-slide-right 0.8s ease forwards;
}
@keyframes mokojoomhero-anim-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes mokojoomhero-anim-slide-up {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes mokojoomhero-anim-slide-left {
from { opacity: 0; transform: translateX(30px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes mokojoomhero-anim-slide-right {
from { opacity: 0; transform: translateX(-30px); }
to { opacity: 1; transform: translateX(0); }
}
/* ============================================================
Parallax
============================================================ */
.mokojoomhero[data-parallax] .mokojoomhero__slide,
.mokojoomhero[data-parallax] .mokojoomhero__color,
.mokojoomhero[data-parallax] .mokojoomhero__poster,
.mokojoomhero[data-parallax] video.mokojoomhero__video,
.mokojoomhero[data-parallax] iframe.mokojoomhero__video {
will-change: transform;
}
/* ============================================================
Reduced motion — WCAG 2.1 AA (SC 2.3.3)
============================================================ */
@media (prefers-reduced-motion: reduce) {
.mokojoomhero__slide {
transition: none !important;
animation: none !important;
}
.mokojoomhero__card[data-card-delay] {
opacity: 1;
animation: none !important;
}
.mokojoomhero__scroll-indicator {
animation: none;
}
.mokojoomhero[data-transition="zoom"] .mokojoomhero__slide--active {
animation: none !important;
}
.mokojoomhero__content[class*="mokojoomhero__content--anim-"] {
opacity: 1;
animation: none !important;
}
.mokojoomhero[data-parallax] .mokojoomhero__slide,
.mokojoomhero[data-parallax] .mokojoomhero__color,
.mokojoomhero[data-parallax] .mokojoomhero__poster,
.mokojoomhero[data-parallax] video.mokojoomhero__video,
.mokojoomhero[data-parallax] iframe.mokojoomhero__video {
will-change: auto;
transform: none !important;
}
}
/* ============================================================
Responsive
============================================================ */
@media (max-width: 768px) {
.mokojoomhero {
height: var(--mokojoomhero-mobile-height, auto) !important;
}
.mokojoomhero__video,
.mokojoomhero__slide {
display: none;
}
/* Keep colour/gradient backgrounds visible on mobile */
.mokojoomhero__color {
position: relative;
}
.mokojoomhero__overlay {
padding: 1rem;
}
/* Only clear overlay background when media is hidden (image/video modes) */
.mokojoomhero:not(:has(.mokojoomhero__color)) .mokojoomhero__overlay {
background-color: transparent !important;
}
.mokojoomhero__content {
font-size: 0.9rem;
}
.mokojoomhero__card {
padding: 1.5rem;
}
}
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -0,0 +1,298 @@
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoJoomHero.Module.Assets
* INGROUP: MokoJoomHero.Module
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
* PATH: /src/packages/mod_mokojoomhero/media/js/mod_mokojoomhero.js
* VERSION: 01.12.00
* BRIEF: Hero module JavaScript — slideshow crossfade, video viewport control, mute toggle
*/
'use strict';
document.addEventListener('DOMContentLoaded', function () {
// Skip slideshow on mobile — video/images are hidden by CSS
if (window.matchMedia('(max-width: 768px)').matches) {
return;
}
var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// ── Image slideshow ──
document.querySelectorAll('.mokojoomhero[data-slides]').forEach(function (hero) {
var slides = hero.querySelectorAll('.mokojoomhero__slide');
var interval = parseInt(hero.dataset.interval, 10) || 5000;
var transition = hero.dataset.transition || 'crossfade';
var current = 0;
// Per-slide content data
var slideContentData = null;
var contentEl = hero.querySelector('.mokojoomhero__content');
if (hero.dataset.slideContent) {
try {
slideContentData = JSON.parse(hero.dataset.slideContent);
} catch (e) {
console.warn('MokoJoomHero: Failed to parse slide content data:', e.message);
slideContentData = null;
}
}
if (slides.length < 2 || prefersReducedMotion) {
return;
}
function updateSlideContent(index) {
if (!slideContentData || !slideContentData[index] || !contentEl) {
return;
}
var data = slideContentData[index];
var card = contentEl.querySelector('.mokojoomhero__card');
var target = card || contentEl;
// Clear existing content safely
while (target.firstChild) {
target.removeChild(target.firstChild);
}
if (data.heading) {
var h2 = document.createElement('h2');
h2.className = 'mokojoomhero__title';
h2.textContent = data.heading;
target.appendChild(h2);
}
if (data.body) {
var p = document.createElement('p');
p.textContent = data.body;
target.appendChild(p);
}
if (data.link && data.linkText) {
var linkP = document.createElement('p');
var a = document.createElement('a');
a.href = data.link;
a.className = 'btn btn-primary';
a.textContent = data.linkText;
linkP.appendChild(a);
target.appendChild(linkP);
}
}
function advanceSlide() {
var prev = current;
current = (current + 1) % slides.length;
if (transition === 'slide') {
slides[prev].classList.add('mokojoomhero__slide--exit');
slides[prev].classList.remove('mokojoomhero__slide--active');
slides[prev].setAttribute('aria-hidden', 'true');
slides[current].classList.add('mokojoomhero__slide--active');
slides[current].setAttribute('aria-hidden', 'false');
// Reset exiting slide after transition completes
setTimeout(function () {
slides[prev].classList.remove('mokojoomhero__slide--exit');
}, 800);
} else if (transition === 'fade-black') {
// Phase 1: fade out current
slides[prev].classList.remove('mokojoomhero__slide--active');
slides[prev].setAttribute('aria-hidden', 'true');
// Phase 2: fade in next after a brief black gap
setTimeout(function () {
slides[current].classList.add('mokojoomhero__slide--active');
slides[current].setAttribute('aria-hidden', 'false');
}, 600);
} else {
// Crossfade and zoom use the same JS logic
slides[prev].classList.remove('mokojoomhero__slide--active');
slides[prev].setAttribute('aria-hidden', 'true');
slides[current].classList.add('mokojoomhero__slide--active');
slides[current].setAttribute('aria-hidden', 'false');
}
updateSlideContent(current);
}
// Set initial slide content
updateSlideContent(0);
setInterval(advanceSlide, interval);
});
// ── Pause/resume videos when out of viewport ──
if (!('IntersectionObserver' in window)) {
return;
}
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
var hero = entry.target;
var video = hero.querySelector('video.mokojoomhero__video');
var iframe = hero.querySelector('iframe.mokojoomhero__video');
if (entry.isIntersecting) {
// Resume
if (video) {
var playPromise = video.play();
if (playPromise !== undefined) {
playPromise.catch(function () {
// Autoplay blocked by browser policy — not actionable
});
}
}
if (iframe && iframe.contentWindow) {
var src = iframe.src || '';
if (src.indexOf('youtube') !== -1) {
iframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*');
} else if (src.indexOf('vimeo') !== -1) {
iframe.contentWindow.postMessage('{"method":"play"}', '*');
}
}
} else {
// Pause
if (video) {
video.pause();
}
if (iframe && iframe.contentWindow) {
var src = iframe.src || '';
if (src.indexOf('youtube') !== -1) {
iframe.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
} else if (src.indexOf('vimeo') !== -1) {
iframe.contentWindow.postMessage('{"method":"pause"}', '*');
}
}
}
});
}, { threshold: 0.1 });
document.querySelectorAll('.mokojoomhero').forEach(function (hero) {
observer.observe(hero);
});
// ── Content entrance animations ──
if (!prefersReducedMotion) {
var animObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add('mokojoomhero__content--visible');
animObserver.unobserve(entry.target);
}
});
}, { threshold: 0.2 });
document.querySelectorAll('.mokojoomhero__content[class*="mokojoomhero__content--anim-"]').forEach(function (el) {
animObserver.observe(el);
});
}
// ── Parallax scroll ──
if (!prefersReducedMotion) {
var parallaxHeroes = document.querySelectorAll('.mokojoomhero[data-parallax]');
if (parallaxHeroes.length) {
var onScroll = function () {
parallaxHeroes.forEach(function (hero) {
var rect = hero.getBoundingClientRect();
var speed = parseFloat(hero.dataset.parallax) || 0.5;
if (rect.bottom > 0 && rect.top < window.innerHeight) {
var offset = Math.round(rect.top * speed * -1);
var bg = hero.querySelector('.mokojoomhero__slide, .mokojoomhero__color, .mokojoomhero__poster, video.mokojoomhero__video');
if (bg) {
bg.style.transform = 'translateY(' + offset + 'px)';
}
var iframe = hero.querySelector('iframe.mokojoomhero__video');
if (iframe) {
iframe.style.transform = 'translate(-50%, calc(-50% + ' + offset + 'px))';
}
}
});
};
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
}
}
// ── Scroll-down indicator ──
document.querySelectorAll('.mokojoomhero__scroll-indicator').forEach(function (btn) {
var hero = btn.closest('.mokojoomhero');
if (!hero) {
return;
}
btn.addEventListener('click', function () {
var nextEl = hero.nextElementSibling || hero.parentElement.nextElementSibling;
if (nextEl) {
nextEl.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth' });
}
});
// Hide indicator once hero scrolls out of view
var scrollObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) {
btn.classList.add('mokojoomhero__scroll-indicator--hidden');
} else {
btn.classList.remove('mokojoomhero__scroll-indicator--hidden');
}
});
}, { threshold: 0.1 });
scrollObserver.observe(hero);
});
// ── Mute/unmute toggle ──
document.querySelectorAll('.mokojoomhero__mute-toggle').forEach(function (btn) {
var hero = btn.closest('.mokojoomhero');
if (!hero) {
return;
}
var video = hero.querySelector('video.mokojoomhero__video');
var iframe = hero.querySelector('iframe.mokojoomhero__video');
var icon = btn.querySelector('.mokojoomhero__mute-icon');
btn.addEventListener('click', function () {
var muted = btn.getAttribute('data-muted') === 'true';
if (video) {
video.muted = !muted;
}
if (iframe && iframe.contentWindow) {
var src = iframe.src || '';
if (src.indexOf('youtube') !== -1) {
var func = muted ? 'unMute' : 'mute';
iframe.contentWindow.postMessage('{"event":"command","func":"' + func + '","args":""}', '*');
} else if (src.indexOf('vimeo') !== -1) {
var vol = muted ? 1 : 0;
iframe.contentWindow.postMessage('{"method":"setVolume","value":' + vol + '}', '*');
}
}
btn.setAttribute('data-muted', muted ? 'false' : 'true');
btn.setAttribute('aria-label', muted ? 'Mute video' : 'Unmute video');
if (icon) {
icon.textContent = muted ? '\u{1F50A}' : '\u{1F507}';
}
});
});
});
@@ -0,0 +1,314 @@
<?php
/**
* @package Joomla.Site
* @subpackage mod_mokojoomhero
*
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\CMS\Uri\Uri;
/** @var \Joomla\CMS\Application\SiteApplication $app */
/** @var \stdClass $module */
/** @var \Joomla\Registry\Registry $params */
// Load module assets via Web Asset Manager (registry in media/mod_mokojoomhero/)
$wa = $app->getDocument()->getWebAssetManager();
$wa->getRegistry()->addExtensionRegistryFile('mod_mokojoomhero');
$wa->usePreset('mod_mokojoomhero');
// Schedule check — skip rendering if outside the configured date range
$scheduleEnabled = (bool) $params->get('scheduleEnabled', 0);
if ($scheduleEnabled) {
$now = new \DateTime('now', new \DateTimeZone($app->get('offset', 'UTC')));
$scheduleStart = $params->get('scheduleStart', '');
$scheduleEnd = $params->get('scheduleEnd', '');
if ($scheduleStart) {
$start = new \DateTime($scheduleStart, new \DateTimeZone($app->get('offset', 'UTC')));
if ($now < $start) {
return;
}
}
if ($scheduleEnd) {
$end = new \DateTime($scheduleEnd, new \DateTimeZone($app->get('offset', 'UTC')));
if ($now > $end) {
return;
}
}
}
// A/B testing — weighted random variation, session-sticky per module instance
$abEnabled = (bool) $params->get('abEnabled', 0);
$abVariationContent = '';
if ($abEnabled) {
$abVariations = $params->get('abVariations', '');
$abData = is_string($abVariations) ? json_decode($abVariations, true) : (array) $abVariations;
if (is_array($abData) && count($abData) > 0) {
$session = \Joomla\CMS\Factory::getSession();
$sessionKey = 'mokojoomhero.ab.' . $module->id;
$picked = $session->get($sessionKey, null);
if ($picked === null || !isset($abData[$picked])) {
// Weighted random selection
$totalWeight = 0;
foreach ($abData as $v) {
$totalWeight += (int) (((array) $v)['weight'] ?? 50);
}
$rand = mt_rand(1, max($totalWeight, 1));
$cumulative = 0;
$picked = 0;
foreach ($abData as $i => $v) {
$cumulative += (int) (((array) $v)['weight'] ?? 50);
if ($rand <= $cumulative) {
$picked = $i;
break;
}
}
$session->set($sessionKey, $picked);
}
$variation = (array) ($abData[$picked] ?? []);
$abVariationContent = $variation['content'] ?? '';
}
}
// Module parameters
$heroMode = $params->get('heroMode', 'images');
$imageFolder = $params->get('imageFolder', 'images/heroes');
$imageCount = (int) $params->get('imageCount', 5);
$slideInterval = (int) $params->get('slideInterval', 5000);
$fadeType = $params->get('fadeType', 'crossfade');
$videoFile = $params->get('videoFile', '');
$heroHeight = $params->get('heroHeight', '60vh');
$heroHeightMobile = $params->get('heroHeightMobile', '');
$overlayColor = $params->get('overlayColor', '#000000');
$overlayType = $params->get('overlayType', 'solid');
$overlayOpacity = (float) $params->get('overlayOpacity', 0.5);
$textAlign = $params->get('textAlign', 'center');
$verticalAlign = $params->get('verticalAlign', 'center');
$textColor = $params->get('textColor', '#ffffff');
$contentSource = $params->get('contentSource', 'manual');
$articleId = (int) $params->get('articleId', 0);
$useArticleTitle = (bool) $params->get('useArticleTitle', 0);
$heroContent = $params->get('heroContent', '');
$slideContent = $params->get('slideContent', '');
$showCard = (bool) $params->get('showCard', 1);
$cardDelay = (int) $params->get('cardDelay', 0);
$contentAnimation = $params->get('contentAnimation', 'none');
$contentAnimationDelay = (int) $params->get('contentAnimationDelay', 0);
$parallaxEnabled = (bool) $params->get('parallaxEnabled', 0);
$parallaxSpeed = (float) $params->get('parallaxSpeed', 0.5);
$showMuteToggle = (bool) $params->get('showMuteToggle', 0);
$videoPoster = $params->get('videoPoster', '');
$showScrollIndicator = (bool) $params->get('showScrollIndicator', 0);
$localVideoFile = $params->get('localVideoFile', '');
$bgColor = $params->get('bgColor', '#003366');
$gradientStart = $params->get('gradientStart', '#003366');
$gradientEnd = $params->get('gradientEnd', '#006699');
$gradientAngle = (int) $params->get('gradientAngle', 135);
// Validate CSS height values to prevent injection
if (!preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeight)) {
$heroHeight = '60vh';
}
if ($heroHeightMobile && !preg_match('/^\d+(\.\d+)?(px|vh|vw|em|rem|%)$/', $heroHeightMobile)) {
$heroHeightMobile = '';
}
// Validate hex colour values
$hexColorPattern = '/^#[0-9a-fA-F]{6}$/';
if (!preg_match($hexColorPattern, $overlayColor)) {
$overlayColor = '#000000';
}
if (!preg_match($hexColorPattern, $textColor)) {
$textColor = '#ffffff';
}
if (!preg_match($hexColorPattern, $bgColor)) {
$bgColor = '#003366';
}
if (!preg_match($hexColorPattern, $gradientStart)) {
$gradientStart = '#003366';
}
if (!preg_match($hexColorPattern, $gradientEnd)) {
$gradientEnd = '#006699';
}
// Validate allowlist values
$allowedTextAlign = ['left', 'center', 'right'];
if (!in_array($textAlign, $allowedTextAlign, true)) {
$textAlign = 'center';
}
$allowedFadeTypes = ['crossfade', 'slide', 'fade-black', 'zoom'];
if (!in_array($fadeType, $allowedFadeTypes, true)) {
$fadeType = 'crossfade';
}
$allowedOverlayTypes = ['solid', 'gradient-bottom', 'gradient-top', 'gradient-left', 'gradient-right'];
if (!in_array($overlayType, $allowedOverlayTypes, true)) {
$overlayType = 'solid';
}
$allowedContentAnimations = ['none', 'fade-in', 'slide-up', 'slide-left', 'slide-right'];
if (!in_array($contentAnimation, $allowedContentAnimations, true)) {
$contentAnimation = 'none';
}
$parallaxSpeed = max(0.1, min(0.9, $parallaxSpeed));
$gradientAngle = max(0, min(360, $gradientAngle));
// Apply A/B variation content if active
if ($abEnabled && $abVariationContent) {
$heroContent = $abVariationContent;
}
// Collect hero images
$heroImages = [];
if ($heroMode === 'images') {
$folderPath = JPATH_ROOT . '/' . ltrim($imageFolder, '/');
if (is_dir($folderPath)) {
try {
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg'];
$all = [];
foreach (new DirectoryIterator($folderPath) as $file) {
if ($file->isFile() && in_array(strtolower($file->getExtension()), $allowed, true)) {
$all[] = $file->getFilename();
}
}
if ($all) {
shuffle($all);
$picked = array_slice($all, 0, min($imageCount, 5));
foreach ($picked as $filename) {
$heroImages[] = Uri::root() . $imageFolder . '/' . $filename;
}
}
} catch (\UnexpectedValueException $e) {
\Joomla\CMS\Log\Log::add(
'MokoJoomHero: Cannot read image folder "' . $folderPath . '": ' . $e->getMessage(),
\Joomla\CMS\Log\Log::WARNING,
'mod_mokojoomhero'
);
}
}
}
// Build video URL — smartly detect YouTube, Vimeo, or local/direct file
$videoUrl = '';
$youtubeId = '';
$vimeoId = '';
if ($heroMode === 'localvideo' && $localVideoFile) {
$videoUrl = Uri::root() . ltrim($localVideoFile, '/');
} elseif ($heroMode === 'video' && $videoFile) {
// YouTube: watch, embed, shorts, youtu.be, with optional timestamps/params
if (preg_match('/(?:youtube\.com\/(?:watch\?.*v=|embed\/|shorts\/|v\/)|youtu\.be\/)([\w-]{11})/', $videoFile, $m)) {
$youtubeId = $m[1];
// Vimeo: vimeo.com/123456 or player.vimeo.com/video/123456
} elseif (preg_match('/vimeo\.com\/(?:video\/)?(\d+)/', $videoFile, $m)) {
$vimeoId = $m[1];
} else {
// Direct URL or local file path
$videoUrl = (strpos($videoFile, '://') !== false)
? $videoFile
: Uri::root() . ltrim($videoFile, '/');
}
}
// Load content from article if configured
$articleTitle = '';
if ($contentSource === 'article' && $articleId > 0) {
try {
$db = \Joomla\CMS\Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['title', 'introtext', 'fulltext']))
->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = ' . $articleId)
->where($db->quoteName('state') . ' = 1');
$db->setQuery($query);
$article = $db->loadObject();
if ($article) {
$rawContent = $article->introtext ?: $article->fulltext;
$heroContent = \Joomla\CMS\HTML\HTMLHelper::_('content.prepare', $rawContent);
$articleTitle = $article->title;
}
} catch (\RuntimeException $e) {
\Joomla\CMS\Log\Log::add(
'MokoJoomHero: Failed to load article ID ' . $articleId . ': ' . $e->getMessage(),
\Joomla\CMS\Log\Log::WARNING,
'mod_mokojoomhero'
);
}
}
// Process per-slide content — overrides folder-based images when populated
$slides = [];
if ($heroMode === 'images' && !empty($slideContent)) {
$slideData = is_string($slideContent) ? json_decode($slideContent, true) : (array) $slideContent;
if ($slideData === null && json_last_error() !== JSON_ERROR_NONE) {
\Joomla\CMS\Log\Log::add(
'MokoJoomHero: Failed to decode slideContent JSON: ' . json_last_error_msg(),
\Joomla\CMS\Log\Log::WARNING,
'mod_mokojoomhero'
);
}
if (is_array($slideData)) {
foreach ($slideData as $item) {
$item = (array) $item;
if (!empty($item['image'])) {
$slides[] = [
'image' => Uri::root() . ltrim($item['image'], '/'),
'heading' => $item['heading'] ?? '',
'body' => $item['body'] ?? '',
'link' => $item['link'] ?? '',
'linkText' => $item['linkText'] ?? 'Learn More',
];
}
}
}
// Per-slide content overrides folder-based random images
if ($slides) {
$heroImages = array_column($slides, 'image');
}
}
require ModuleHelper::getLayoutPath('mod_mokojoomhero', $params->get('layout', 'default'));
@@ -0,0 +1,520 @@
<?xml version="1.0" encoding="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: MokoJoomHero.Module
INGROUP: MokoJoomHero
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero
PATH: /src/packages/mod_mokojoomhero/mod_mokojoomhero.xml
VERSION: 01.00.20
BRIEF: Joomla module manifest — random hero image with content overlay
-->
<extension type="module" client="site" method="upgrade">
<name>Module - MokoJoomHero</name>
<creationDate>2026-05</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<version>01.12.00</version>
<description>Displays a random hero image slideshow or background video with content overlaid. Designed for MokoOnyx template. By Moko Consulting.</description>
<scriptfile>script.php</scriptfile>
<files>
<filename module="mod_mokojoomhero">mod_mokojoomhero.php</filename>
<filename>mod_mokojoomhero.xml</filename>
<filename>script.php</filename>
<folder>tmpl</folder>
<folder>language</folder>
</files>
<media destination="mod_mokojoomhero" folder="media">
<filename>joomla.asset.json</filename>
<folder>css</folder>
<folder>js</folder>
</media>
<languages folder="language">
<language tag="en-GB">en-GB/mod_mokojoomhero.ini</language>
<language tag="en-GB">en-GB/mod_mokojoomhero.sys.ini</language>
<language tag="en-US">en-US/mod_mokojoomhero.ini</language>
<language tag="en-US">en-US/mod_mokojoomhero.sys.ini</language>
</languages>
<config>
<fields name="params">
<fieldset name="basic">
<field
name="heroMode"
type="list"
label="MOD_MOKOJOOMHERO_MODE_LABEL"
description="MOD_MOKOJOOMHERO_MODE_DESC"
default="images"
>
<option value="images">MOD_MOKOJOOMHERO_MODE_IMAGES</option>
<option value="video">MOD_MOKOJOOMHERO_MODE_VIDEO</option>
<option value="localvideo">MOD_MOKOJOOMHERO_MODE_LOCALVIDEO</option>
<option value="color">MOD_MOKOJOOMHERO_MODE_COLOR</option>
<option value="gradient">MOD_MOKOJOOMHERO_MODE_GRADIENT</option>
</field>
<field
name="bgColor"
type="color"
label="MOD_MOKOJOOMHERO_BG_COLOR_LABEL"
description="MOD_MOKOJOOMHERO_BG_COLOR_DESC"
default="#003366"
showon="heroMode:color"
/>
<field
name="gradientStart"
type="color"
label="MOD_MOKOJOOMHERO_GRADIENT_START_LABEL"
description="MOD_MOKOJOOMHERO_GRADIENT_START_DESC"
default="#003366"
showon="heroMode:gradient"
/>
<field
name="gradientEnd"
type="color"
label="MOD_MOKOJOOMHERO_GRADIENT_END_LABEL"
description="MOD_MOKOJOOMHERO_GRADIENT_END_DESC"
default="#006699"
showon="heroMode:gradient"
/>
<field
name="gradientAngle"
type="number"
label="MOD_MOKOJOOMHERO_GRADIENT_ANGLE_LABEL"
description="MOD_MOKOJOOMHERO_GRADIENT_ANGLE_DESC"
default="135"
min="0"
max="360"
step="15"
showon="heroMode:gradient"
/>
<field
name="fadeType"
type="list"
label="MOD_MOKOJOOMHERO_FADE_TYPE_LABEL"
description="MOD_MOKOJOOMHERO_FADE_TYPE_DESC"
default="crossfade"
showon="heroMode:images"
>
<option value="crossfade">MOD_MOKOJOOMHERO_FADE_CROSSFADE</option>
<option value="slide">MOD_MOKOJOOMHERO_FADE_SLIDE</option>
<option value="fade-black">MOD_MOKOJOOMHERO_FADE_BLACK</option>
<option value="zoom">MOD_MOKOJOOMHERO_FADE_ZOOM</option>
</field>
<field
name="imageFolder"
type="text"
label="MOD_MOKOJOOMHERO_IMAGE_FOLDER_LABEL"
description="MOD_MOKOJOOMHERO_IMAGE_FOLDER_DESC"
default="images/heroes"
filter="path"
showon="heroMode:images"
/>
<field
name="imageCount"
type="number"
label="MOD_MOKOJOOMHERO_IMAGE_COUNT_LABEL"
description="MOD_MOKOJOOMHERO_IMAGE_COUNT_DESC"
default="5"
min="1"
max="5"
showon="heroMode:images"
/>
<field
name="slideInterval"
type="number"
label="MOD_MOKOJOOMHERO_SLIDE_INTERVAL_LABEL"
description="MOD_MOKOJOOMHERO_SLIDE_INTERVAL_DESC"
default="5000"
min="1000"
step="500"
showon="heroMode:images"
/>
<field
name="slideContent"
type="subform"
label="MOD_MOKOJOOMHERO_SLIDE_CONTENT_LABEL"
description="MOD_MOKOJOOMHERO_SLIDE_CONTENT_DESC"
multiple="true"
layout="joomla.form.field.subform.repeatable-table"
showon="heroMode:images"
max="5"
>
<form>
<field
name="image"
type="media"
label="MOD_MOKOJOOMHERO_SLIDE_IMAGE_LABEL"
types="images"
/>
<field
name="heading"
type="text"
label="MOD_MOKOJOOMHERO_SLIDE_HEADING_LABEL"
filter="string"
/>
<field
name="body"
type="textarea"
label="MOD_MOKOJOOMHERO_SLIDE_BODY_LABEL"
filter="safehtml"
rows="3"
/>
<field
name="link"
type="url"
label="MOD_MOKOJOOMHERO_SLIDE_LINK_LABEL"
filter="url"
/>
<field
name="linkText"
type="text"
label="MOD_MOKOJOOMHERO_SLIDE_LINK_TEXT_LABEL"
filter="string"
default="Learn More"
/>
</form>
</field>
<field
name="videoFile"
type="text"
label="MOD_MOKOJOOMHERO_VIDEO_FILE_LABEL"
description="MOD_MOKOJOOMHERO_VIDEO_FILE_DESC"
filter="string"
showon="heroMode:video"
/>
<field
name="localVideoFile"
type="media"
label="MOD_MOKOJOOMHERO_LOCAL_VIDEO_LABEL"
description="MOD_MOKOJOOMHERO_LOCAL_VIDEO_DESC"
types="videos"
showon="heroMode:localvideo"
/>
<field
name="heroHeight"
type="text"
label="MOD_MOKOJOOMHERO_HERO_HEIGHT_LABEL"
description="MOD_MOKOJOOMHERO_HERO_HEIGHT_DESC"
hint="MOD_MOKOJOOMHERO_HERO_HEIGHT_HINT"
default="60vh"
filter="string"
/>
<field
name="heroHeightMobile"
type="text"
label="MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_LABEL"
description="MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_DESC"
hint="MOD_MOKOJOOMHERO_HERO_HEIGHT_MOBILE_HINT"
default=""
filter="string"
/>
<field
name="videoPoster"
type="media"
label="MOD_MOKOJOOMHERO_VIDEO_POSTER_LABEL"
description="MOD_MOKOJOOMHERO_VIDEO_POSTER_DESC"
types="images"
showon="heroMode:video,localvideo"
/>
<field
name="showScrollIndicator"
type="radio"
layout="joomla.form.field.radio.switcher"
label="MOD_MOKOJOOMHERO_SCROLL_INDICATOR_LABEL"
description="MOD_MOKOJOOMHERO_SCROLL_INDICATOR_DESC"
default="0"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="parallaxEnabled"
type="radio"
layout="joomla.form.field.radio.switcher"
label="MOD_MOKOJOOMHERO_PARALLAX_LABEL"
description="MOD_MOKOJOOMHERO_PARALLAX_DESC"
default="0"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="parallaxSpeed"
type="range"
label="MOD_MOKOJOOMHERO_PARALLAX_SPEED_LABEL"
description="MOD_MOKOJOOMHERO_PARALLAX_SPEED_DESC"
default="0.5"
min="0.1"
max="0.9"
step="0.1"
showon="parallaxEnabled:1"
/>
<field
name="showMuteToggle"
type="radio"
layout="joomla.form.field.radio.switcher"
label="MOD_MOKOJOOMHERO_MUTE_TOGGLE_LABEL"
description="MOD_MOKOJOOMHERO_MUTE_TOGGLE_DESC"
default="0"
showon="heroMode:video,localvideo"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
</fieldset>
<fieldset name="abtesting"
label="MOD_MOKOJOOMHERO_FIELDSET_AB"
>
<field
name="abEnabled"
type="radio"
layout="joomla.form.field.radio.switcher"
label="MOD_MOKOJOOMHERO_AB_ENABLED_LABEL"
description="MOD_MOKOJOOMHERO_AB_ENABLED_DESC"
default="0"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="abVariations"
type="subform"
label="MOD_MOKOJOOMHERO_AB_VARIATIONS_LABEL"
description="MOD_MOKOJOOMHERO_AB_VARIATIONS_DESC"
multiple="true"
layout="joomla.form.field.subform.repeatable-table"
showon="abEnabled:1"
max="4"
>
<form>
<field
name="label"
type="text"
label="MOD_MOKOJOOMHERO_AB_VAR_LABEL"
filter="string"
default="Variation"
/>
<field
name="content"
type="textarea"
label="MOD_MOKOJOOMHERO_AB_VAR_CONTENT"
filter="safehtml"
rows="3"
/>
<field
name="weight"
type="number"
label="MOD_MOKOJOOMHERO_AB_VAR_WEIGHT"
default="50"
min="1"
max="100"
/>
</form>
</field>
</fieldset>
<fieldset name="scheduling"
label="MOD_MOKOJOOMHERO_FIELDSET_SCHEDULING"
>
<field
name="scheduleEnabled"
type="radio"
layout="joomla.form.field.radio.switcher"
label="MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_LABEL"
description="MOD_MOKOJOOMHERO_SCHEDULE_ENABLED_DESC"
default="0"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="scheduleStart"
type="calendar"
label="MOD_MOKOJOOMHERO_SCHEDULE_START_LABEL"
description="MOD_MOKOJOOMHERO_SCHEDULE_START_DESC"
format="%Y-%m-%d %H:%M"
showtime="true"
showon="scheduleEnabled:1"
/>
<field
name="scheduleEnd"
type="calendar"
label="MOD_MOKOJOOMHERO_SCHEDULE_END_LABEL"
description="MOD_MOKOJOOMHERO_SCHEDULE_END_DESC"
format="%Y-%m-%d %H:%M"
showtime="true"
showon="scheduleEnabled:1"
/>
</fieldset>
<fieldset name="content"
label="MOD_MOKOJOOMHERO_FIELDSET_CONTENT"
>
<field
name="contentSource"
type="list"
label="MOD_MOKOJOOMHERO_CONTENT_SOURCE_LABEL"
description="MOD_MOKOJOOMHERO_CONTENT_SOURCE_DESC"
default="manual"
>
<option value="manual">MOD_MOKOJOOMHERO_SOURCE_MANUAL</option>
<option value="article">MOD_MOKOJOOMHERO_SOURCE_ARTICLE</option>
</field>
<field
name="heroContent"
type="editor"
label="MOD_MOKOJOOMHERO_CONTENT_LABEL"
description="MOD_MOKOJOOMHERO_CONTENT_DESC"
filter="safehtml"
buttons="true"
hide="readmore,pagebreak"
showon="contentSource:manual"
/>
<field
name="articleId"
type="sql"
label="MOD_MOKOJOOMHERO_ARTICLE_LABEL"
description="MOD_MOKOJOOMHERO_ARTICLE_DESC"
query="SELECT id, title FROM #__content WHERE state = 1 ORDER BY title ASC"
key_field="id"
value_field="title"
header="MOD_MOKOJOOMHERO_ARTICLE_SELECT"
showon="contentSource:article"
/>
<field
name="useArticleTitle"
type="radio"
layout="joomla.form.field.radio.switcher"
label="MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_LABEL"
description="MOD_MOKOJOOMHERO_USE_ARTICLE_TITLE_DESC"
default="0"
showon="contentSource:article"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="showCard"
type="radio"
layout="joomla.form.field.radio.switcher"
label="MOD_MOKOJOOMHERO_SHOW_CARD_LABEL"
description="MOD_MOKOJOOMHERO_SHOW_CARD_DESC"
default="1"
>
<option value="0">JNO</option>
<option value="1">JYES</option>
</field>
<field
name="contentAnimation"
type="list"
label="MOD_MOKOJOOMHERO_CONTENT_ANIM_LABEL"
description="MOD_MOKOJOOMHERO_CONTENT_ANIM_DESC"
default="none"
>
<option value="none">MOD_MOKOJOOMHERO_ANIM_NONE</option>
<option value="fade-in">MOD_MOKOJOOMHERO_ANIM_FADE_IN</option>
<option value="slide-up">MOD_MOKOJOOMHERO_ANIM_SLIDE_UP</option>
<option value="slide-left">MOD_MOKOJOOMHERO_ANIM_SLIDE_LEFT</option>
<option value="slide-right">MOD_MOKOJOOMHERO_ANIM_SLIDE_RIGHT</option>
</field>
<field
name="contentAnimationDelay"
type="number"
label="MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_LABEL"
description="MOD_MOKOJOOMHERO_CONTENT_ANIM_DELAY_DESC"
default="0"
min="0"
max="3000"
step="100"
showon="contentAnimation!:none"
/>
<field
name="cardDelay"
type="number"
label="MOD_MOKOJOOMHERO_CARD_DELAY_LABEL"
description="MOD_MOKOJOOMHERO_CARD_DELAY_DESC"
default="0"
min="0"
max="5000"
step="250"
showon="showCard:1"
/>
</fieldset>
<fieldset name="advanced"
label="MOD_MOKOJOOMHERO_FIELDSET_OVERLAY"
>
<field
name="overlayColor"
type="color"
label="MOD_MOKOJOOMHERO_OVERLAY_COLOR_LABEL"
description="MOD_MOKOJOOMHERO_OVERLAY_COLOR_DESC"
default="#000000"
/>
<field
name="overlayType"
type="list"
label="MOD_MOKOJOOMHERO_OVERLAY_TYPE_LABEL"
description="MOD_MOKOJOOMHERO_OVERLAY_TYPE_DESC"
default="solid"
>
<option value="solid">MOD_MOKOJOOMHERO_OVERLAY_SOLID</option>
<option value="gradient-bottom">MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_BOTTOM</option>
<option value="gradient-top">MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_TOP</option>
<option value="gradient-left">MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_LEFT</option>
<option value="gradient-right">MOD_MOKOJOOMHERO_OVERLAY_GRADIENT_RIGHT</option>
</field>
<field
name="overlayOpacity"
type="range"
label="MOD_MOKOJOOMHERO_OVERLAY_OPACITY_LABEL"
description="MOD_MOKOJOOMHERO_OVERLAY_OPACITY_DESC"
default="0.5"
min="0"
max="1"
step="0.1"
/>
<field
name="textAlign"
type="list"
label="MOD_MOKOJOOMHERO_TEXT_ALIGN_LABEL"
description="MOD_MOKOJOOMHERO_TEXT_ALIGN_DESC"
default="center"
>
<option value="left">MOD_MOKOJOOMHERO_ALIGN_LEFT</option>
<option value="center">MOD_MOKOJOOMHERO_ALIGN_CENTER</option>
<option value="right">MOD_MOKOJOOMHERO_ALIGN_RIGHT</option>
</field>
<field
name="verticalAlign"
type="list"
label="MOD_MOKOJOOMHERO_VALIGN_LABEL"
description="MOD_MOKOJOOMHERO_VALIGN_DESC"
default="center"
>
<option value="top">MOD_MOKOJOOMHERO_VALIGN_TOP</option>
<option value="center">MOD_MOKOJOOMHERO_VALIGN_CENTER</option>
<option value="bottom">MOD_MOKOJOOMHERO_VALIGN_BOTTOM</option>
</field>
<field
name="textColor"
type="color"
label="MOD_MOKOJOOMHERO_TEXT_COLOR_LABEL"
description="MOD_MOKOJOOMHERO_TEXT_COLOR_DESC"
default="#ffffff"
/>
</fieldset>
</fields>
</config>
</extension>
@@ -6,6 +6,7 @@
*
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
@@ -0,0 +1,163 @@
<?php
/**
* @package Joomla.Site
* @subpackage mod_mokojoomhero
*
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
/** @var string $heroMode */
/** @var array $heroImages */
/** @var int $slideInterval */
/** @var string $fadeType */
/** @var string $videoUrl */
/** @var string $youtubeId */
/** @var string $vimeoId */
/** @var string $heroHeight */
/** @var string $heroHeightMobile */
/** @var string $overlayColor */
/** @var string $overlayType */
/** @var float $overlayOpacity */
/** @var string $textAlign */
/** @var string $verticalAlign */
/** @var string $textColor */
/** @var string $contentSource */
/** @var int $articleId */
/** @var bool $useArticleTitle */
/** @var string $heroContent */
/** @var string $articleTitle */
/** @var array $slides */
/** @var bool $showCard */
/** @var int $cardDelay */
/** @var string $contentAnimation */
/** @var int $contentAnimationDelay */
/** @var bool $parallaxEnabled */
/** @var float $parallaxSpeed */
/** @var bool $showMuteToggle */
/** @var string $videoPoster */
/** @var bool $showScrollIndicator */
/** @var string $bgColor */
/** @var string $gradientStart */
/** @var string $gradientEnd */
/** @var int $gradientAngle */
$moduleId = 'mod-mokojoomhero-' . $module->id;
// Convert hex overlay colour to rgba
$r = hexdec(substr($overlayColor, 1, 2));
$g = hexdec(substr($overlayColor, 3, 2));
$b = hexdec(substr($overlayColor, 5, 2));
$rgbaOpaque = "rgba($r, $g, $b, $overlayOpacity)";
$rgbaTransparent = "rgba($r, $g, $b, 0)";
// Build overlay background based on type
$overlayDirections = [
'gradient-bottom' => 'to bottom',
'gradient-top' => 'to top',
'gradient-left' => 'to left',
'gradient-right' => 'to right',
];
if ($overlayType !== 'solid' && isset($overlayDirections[$overlayType])) {
$dir = $overlayDirections[$overlayType];
$overlayBg = "background: linear-gradient($dir, $rgbaTransparent, $rgbaOpaque);";
} else {
$overlayBg = "background-color: $rgbaOpaque;";
}
// Map vertical alignment to CSS align-items
$valignMap = ['top' => 'flex-start', 'center' => 'center', 'bottom' => 'flex-end'];
$valignCss = $valignMap[$verticalAlign] ?? 'center';
$heightAttr = htmlspecialchars($heroHeight, ENT_QUOTES, 'UTF-8');
?>
<?php if ($heroHeightMobile) : ?>
<style>#<?php echo $moduleId; ?> { --mokojoomhero-mobile-height: <?php echo htmlspecialchars($heroHeightMobile, ENT_QUOTES, 'UTF-8'); ?>; }</style>
<?php endif; ?>
<div id="<?php echo $moduleId; ?>" class="mokojoomhero" style="height: <?php echo $heightAttr; ?>;"
<?php if ($parallaxEnabled) : ?>
data-parallax="<?php echo $parallaxSpeed; ?>"
<?php endif; ?>
<?php if ($heroMode === 'images' && count($heroImages) > 1) : ?>
data-slides="<?php echo htmlspecialchars(json_encode($heroImages), ENT_QUOTES, 'UTF-8'); ?>"
data-interval="<?php echo $slideInterval; ?>"
data-transition="<?php echo htmlspecialchars($fadeType, ENT_QUOTES, 'UTF-8'); ?>"
<?php if ($slides) : ?>
data-slide-content="<?php echo htmlspecialchars(json_encode($slides), ENT_QUOTES, 'UTF-8'); ?>"
<?php endif; ?>
<?php endif; ?>
>
<?php // Background layer — solid colour, single image, slideshow, or video ?>
<?php if ($heroMode === 'color') : ?>
<div class="mokojoomhero__color" style="background-color: <?php echo htmlspecialchars($bgColor, ENT_QUOTES, 'UTF-8'); ?>;"></div>
<?php elseif ($heroMode === 'gradient') : ?>
<div class="mokojoomhero__color" style="background: linear-gradient(<?php echo $gradientAngle; ?>deg, <?php echo htmlspecialchars($gradientStart, ENT_QUOTES, 'UTF-8'); ?>, <?php echo htmlspecialchars($gradientEnd, ENT_QUOTES, 'UTF-8'); ?>);"></div>
<?php elseif ($heroMode === 'video' && $youtubeId) : ?>
<?php if ($videoPoster) : ?>
<div class="mokojoomhero__poster" style="background-image: url('<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root() . ltrim($videoPoster, '/'), ENT_QUOTES, 'UTF-8'); ?>');"></div>
<?php endif; ?>
<iframe class="mokojoomhero__video" src="https://www.youtube-nocookie.com/embed/<?php echo htmlspecialchars($youtubeId, ENT_QUOTES, 'UTF-8'); ?>?autoplay=1&mute=1&loop=1&playlist=<?php echo htmlspecialchars($youtubeId, ENT_QUOTES, 'UTF-8'); ?>&controls=0&showinfo=0&rel=0&modestbranding=1&playsinline=1&enablejsapi=1&origin=<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root(), ENT_QUOTES, 'UTF-8'); ?>" allow="autoplay; encrypted-media" allowfullscreen></iframe>
<?php elseif ($heroMode === 'video' && $vimeoId) : ?>
<?php if ($videoPoster) : ?>
<div class="mokojoomhero__poster" style="background-image: url('<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root() . ltrim($videoPoster, '/'), ENT_QUOTES, 'UTF-8'); ?>');"></div>
<?php endif; ?>
<iframe class="mokojoomhero__video" src="https://player.vimeo.com/video/<?php echo htmlspecialchars($vimeoId, ENT_QUOTES, 'UTF-8'); ?>?autoplay=1&muted=1&loop=1&background=1" allow="autoplay" allowfullscreen></iframe>
<?php elseif (($heroMode === 'video' || $heroMode === 'localvideo') && $videoUrl) : ?>
<?php if ($videoPoster) : ?>
<div class="mokojoomhero__poster" style="background-image: url('<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root() . ltrim($videoPoster, '/'), ENT_QUOTES, 'UTF-8'); ?>');"></div>
<?php endif; ?>
<video class="mokojoomhero__video" autoplay muted loop playsinline<?php if ($videoPoster) : ?> poster="<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root() . ltrim($videoPoster, '/'), ENT_QUOTES, 'UTF-8'); ?>"<?php endif; ?>>
<source src="<?php echo htmlspecialchars($videoUrl, ENT_QUOTES, 'UTF-8'); ?>">
</video>
<?php elseif ($heroImages) : ?>
<?php foreach ($heroImages as $i => $img) : ?>
<div class="mokojoomhero__slide<?php echo $i === 0 ? ' mokojoomhero__slide--active' : ''; ?>"
style="background-image: url('<?php echo htmlspecialchars($img, ENT_QUOTES, 'UTF-8'); ?>');"
aria-hidden="<?php echo $i === 0 ? 'false' : 'true'; ?>">
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php if (($heroMode === 'video' || $heroMode === 'localvideo') && $showMuteToggle) : ?>
<button class="mokojoomhero__mute-toggle" type="button" aria-label="Unmute video" data-muted="true">
<span class="mokojoomhero__mute-icon" aria-hidden="true">&#x1F507;</span>
</button>
<?php endif; ?>
<?php if ($showScrollIndicator) : ?>
<button class="mokojoomhero__scroll-indicator" type="button" aria-label="Scroll down">
<svg class="mokojoomhero__scroll-chevron" viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<?php endif; ?>
<?php // Overlay + content ?>
<div class="mokojoomhero__overlay" style="<?php echo $overlayBg; ?> align-items: <?php echo $valignCss; ?>;">
<div class="mokojoomhero__content<?php if ($contentAnimation !== 'none') : ?> mokojoomhero__content--anim-<?php echo htmlspecialchars($contentAnimation, ENT_QUOTES, 'UTF-8'); ?><?php endif; ?>" style="text-align: <?php echo htmlspecialchars($textAlign, ENT_QUOTES, 'UTF-8'); ?>; color: <?php echo htmlspecialchars($textColor, ENT_QUOTES, 'UTF-8'); ?>;<?php if ($contentAnimationDelay) : ?> animation-delay: <?php echo $contentAnimationDelay; ?>ms;<?php endif; ?>">
<?php
$displayTitle = ($contentSource === 'article' && $useArticleTitle && $articleTitle)
? $articleTitle
: $module->title;
$showTitle = ($contentSource === 'article' && $useArticleTitle && $articleTitle) || $module->showtitle;
?>
<?php if ($heroContent || $showTitle) : ?>
<?php if ($showCard) : ?>
<div class="mokojoomhero__card"<?php if ($cardDelay) : ?> style="animation-delay: <?php echo $cardDelay; ?>ms;" data-card-delay="<?php echo $cardDelay; ?>"<?php endif; ?>>
<?php if ($showTitle) : ?>
<h2 class="mokojoomhero__title"><?php echo htmlspecialchars($displayTitle, ENT_QUOTES, 'UTF-8'); ?></h2>
<?php endif; ?>
<?php echo $heroContent; ?>
</div>
<?php else : ?>
<?php if ($showTitle) : ?>
<h2 class="mokojoomhero__title"><?php echo htmlspecialchars($displayTitle, ENT_QUOTES, 'UTF-8'); ?></h2>
<?php endif; ?>
<?php echo $heroContent; ?>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</div>
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -0,0 +1,2 @@
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
; SPDX-License-Identifier: GPL-3.0-or-later
@@ -0,0 +1,5 @@
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
; SPDX-License-Identifier: GPL-3.0-or-later
PLG_SYSTEM_MOKOJOOMHERO="System - MokoJoomHero"
PLG_SYSTEM_MOKOJOOMHERO_DESCRIPTION="System plugin for MokoJoomHero — license and update management"
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -0,0 +1,2 @@
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
; SPDX-License-Identifier: GPL-3.0-or-later
@@ -0,0 +1,5 @@
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
; SPDX-License-Identifier: GPL-3.0-or-later
PLG_SYSTEM_MOKOJOOMHERO="System - MokoJoomHero"
PLG_SYSTEM_MOKOJOOMHERO_DESCRIPTION="System plugin for MokoJoomHero — license and update management"
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -0,0 +1,12 @@
<?php
/**
* @package MokoJoomHero
* @subpackage plg_system_mokojoomhero
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
* @package MokoJoomHero
* @subpackage plg_system_mokojoomhero
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
-->
<extension type="plugin" group="system" method="upgrade">
<name>PLG_SYSTEM_MOKOJOOMHERO</name>
<version>01.12.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>PLG_SYSTEM_MOKOJOOMHERO_DESCRIPTION</description>
<namespace path="src">Joomla\Plugin\System\MokoJoomHero</namespace>
<files>
<filename plugin="mokojoomhero">mokojoomhero.php</filename>
<folder>services</folder>
<folder>src</folder>
</files>
<languages folder="language">
<language tag="en-GB">en-GB/plg_system_mokojoomhero.ini</language>
<language tag="en-GB">en-GB/plg_system_mokojoomhero.sys.ini</language>
<language tag="en-US">en-US/plg_system_mokojoomhero.ini</language>
<language tag="en-US">en-US/plg_system_mokojoomhero.sys.ini</language>
</languages>
</extension>
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -0,0 +1,39 @@
<?php
/**
* @package MokoJoomHero
* @subpackage plg_system_mokojoomhero
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Extension\PluginInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Plugin\System\MokoJoomHero\Extension\MokoJoomHero;
return new class implements ServiceProviderInterface {
public function register(Container $container): void
{
$container->set(
PluginInterface::class,
function (Container $container) {
$plugin = new MokoJoomHero(
$container->get(DispatcherInterface::class),
(array) PluginHelper::getPlugin('system', 'mokojoomhero')
);
$plugin->setApplication(Factory::getApplication());
return $plugin;
}
);
}
};
@@ -0,0 +1,100 @@
<?php
/**
* @package MokoJoomHero
* @subpackage plg_system_mokojoomhero
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
namespace Joomla\Plugin\System\MokoJoomHero\Extension;
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\Event\SubscriberInterface;
class MokoJoomHero extends CMSPlugin implements SubscriberInterface
{
/**
* License type: 'free' requires no key, 'pro' requires a valid download key.
*/
private const LICENSE_TYPE = 'free';
public static function getSubscribedEvents(): array
{
return [
'onAfterRoute' => 'onAfterRoute',
];
}
public function onAfterRoute(): void
{
$app = $this->getApplication();
if ($app->isClient('administrator')) {
$this->checkLicense();
}
}
/**
* Check license status once per session. Free tier requires no key.
* Pro tier warns if no valid download key is configured.
*/
private function checkLicense(): void
{
if (self::LICENSE_TYPE === 'free') {
return;
}
$session = Factory::getSession();
if ($session->get('mokojoomhero.license_checked', false)) {
return;
}
$user = Factory::getUser();
if ($user->guest || !$user->authorise('core.manage')) {
return;
}
$session->set('mokojoomhero.license_checked', true);
try {
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName('extra_query'))
->from($db->quoteName('#__update_sites'))
->where($db->quoteName('name') . ' = ' . $db->quote('MokoJoomHero Updates'))
->setLimit(1);
$db->setQuery($query);
$extraQuery = (string) $db->loadResult();
if (!empty($extraQuery)) {
parse_str($extraQuery, $parsed);
if (!empty($parsed['dlid']) && preg_match('/^MOKO-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/', $parsed['dlid'])) {
return;
}
}
$this->getApplication()->enqueueMessage(
'<strong>MokoJoomHero — Download Key Required</strong> — '
. 'No download key is configured. Updates may not be available until a valid key is entered. '
. 'Go to <a href="index.php?option=com_installer&view=updatesites">System &rarr; Update Sites</a> '
. 'and enter your download key (<code>MOKO-XXXX-XXXX-XXXX-XXXX</code>) for the MokoJoomHero update site.',
'warning'
);
} catch (\RuntimeException $e) {
$this->getApplication()->getLogger()->warning(
'MokoJoomHero license check failed: ' . $e->getMessage(),
['exception' => $e]
);
}
}
}
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
* @package MokoJoomHero
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
-->
<extension type="package" method="upgrade">
<name>Package - MokoJoomHero</name>
<packagename>mokojoomhero</packagename>
<version>01.12.00</version>
<creationDate>2026-06-02</creationDate>
<author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
<license>GPL-3.0-or-later</license>
<description>Random hero image slideshow or background video with content overlay. Includes the hero module and system plugin. By Moko Consulting.</description>
<scriptfile>pkg_script.php</scriptfile>
<files folder="packages">
<file type="module" id="mod_mokojoomhero" client="site">mod_mokojoomhero.zip</file>
<file type="plugin" id="mokojoomhero" group="system">plg_system_mokojoomhero.zip</file>
</files>
<updateservers>
<server type="extension" name="MokoJoomHero Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/updates.xml</server>
</updateservers>
</extension>
+59
View File
@@ -0,0 +1,59 @@
<?php
/**
* @package MokoJoomHero
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
* SPDX-License-Identifier: GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Installer\InstallerAdapter;
class Pkg_MokoJoomHeroInstallerScript
{
/**
* Called after install/update — only enables the system plugin on fresh install.
*
* @param string $type Action type
* @param InstallerAdapter $parent Installer adapter
*
* @return void
*/
public function postflight(string $type, InstallerAdapter $parent): void
{
if ($type === 'install') {
try {
$db = Factory::getDbo();
// Enable the system plugin automatically on fresh install
$query = $db->getQuery(true)
->update($db->quoteName('#__extensions'))
->set($db->quoteName('enabled') . ' = 1')
->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
->where($db->quoteName('element') . ' = ' . $db->quote('mokojoomhero'));
$db->setQuery($query);
$db->execute();
if ($db->getAffectedRows() === 0) {
Factory::getApplication()->enqueueMessage(
'MokoJoomHero: The system plugin could not be auto-enabled. '
. 'Please enable it manually in Extensions &rarr; Plugins.',
'warning'
);
}
} catch (\Exception $e) {
Factory::getApplication()->enqueueMessage(
'MokoJoomHero: Failed to auto-enable system plugin: ' . $e->getMessage()
. ' — Please enable it manually in Extensions &rarr; Plugins.',
'warning'
);
}
}
}
}
-92
View File
@@ -1,92 +0,0 @@
<?php
/**
* @package Joomla.Site
* @subpackage mod_mokojoomhero
*
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
/** @var string $heroMode */
/** @var array $heroImages */
/** @var int $slideInterval */
/** @var string $videoUrl */
/** @var string $youtubeId */
/** @var string $vimeoId */
/** @var string $heroHeight */
/** @var string $overlayColor */
/** @var float $overlayOpacity */
/** @var string $textAlign */
/** @var string $textColor */
/** @var string $heroContent */
/** @var bool $showCard */
/** @var int $cardDelay */
/** @var bool $showMuteToggle */
/** @var string $content */
$moduleId = 'mod-mokojoomhero-' . $module->id;
// Convert hex overlay colour to rgba
$r = hexdec(substr($overlayColor, 1, 2));
$g = hexdec(substr($overlayColor, 3, 2));
$b = hexdec(substr($overlayColor, 5, 2));
$rgba = "rgba($r, $g, $b, $overlayOpacity)";
$heightAttr = htmlspecialchars($heroHeight, ENT_QUOTES, 'UTF-8');
?>
<div id="<?php echo $moduleId; ?>" class="mokojoomhero" style="height: <?php echo $heightAttr; ?>;"
<?php if ($heroMode === 'images' && count($heroImages) > 1) : ?>
data-slides="<?php echo htmlspecialchars(json_encode($heroImages), ENT_QUOTES, 'UTF-8'); ?>"
data-interval="<?php echo $slideInterval; ?>"
<?php endif; ?>
>
<?php // Background layer — single image, slideshow, or video ?>
<?php if ($heroMode === 'video' && $youtubeId) : ?>
<iframe class="mokojoomhero__video" src="https://www.youtube-nocookie.com/embed/<?php echo htmlspecialchars($youtubeId, ENT_QUOTES, 'UTF-8'); ?>?autoplay=1&mute=1&loop=1&playlist=<?php echo htmlspecialchars($youtubeId, ENT_QUOTES, 'UTF-8'); ?>&controls=0&showinfo=0&rel=0&modestbranding=1&playsinline=1&enablejsapi=1&origin=<?php echo htmlspecialchars(\Joomla\CMS\Uri\Uri::root(), ENT_QUOTES, 'UTF-8'); ?>" allow="autoplay; encrypted-media" allowfullscreen></iframe>
<?php elseif ($heroMode === 'video' && $vimeoId) : ?>
<iframe class="mokojoomhero__video" src="https://player.vimeo.com/video/<?php echo htmlspecialchars($vimeoId, ENT_QUOTES, 'UTF-8'); ?>?autoplay=1&muted=1&loop=1&background=1" allow="autoplay" allowfullscreen></iframe>
<?php elseif (($heroMode === 'video' || $heroMode === 'localvideo') && $videoUrl) : ?>
<video class="mokojoomhero__video" autoplay muted loop playsinline>
<source src="<?php echo htmlspecialchars($videoUrl, ENT_QUOTES, 'UTF-8'); ?>">
</video>
<?php elseif ($heroImages) : ?>
<?php foreach ($heroImages as $i => $img) : ?>
<div class="mokojoomhero__slide<?php echo $i === 0 ? ' mokojoomhero__slide--active' : ''; ?>"
style="background-image: url('<?php echo htmlspecialchars($img, ENT_QUOTES, 'UTF-8'); ?>');"
aria-hidden="<?php echo $i === 0 ? 'false' : 'true'; ?>">
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php if (($heroMode === 'video' || $heroMode === 'localvideo') && $showMuteToggle) : ?>
<button class="mokojoomhero__mute-toggle" type="button" aria-label="Unmute video" data-muted="true">
<span class="mokojoomhero__mute-icon" aria-hidden="true">&#x1F507;</span>
</button>
<?php endif; ?>
<?php // Overlay + content ?>
<div class="mokojoomhero__overlay" style="background-color: <?php echo $rgba; ?>;">
<div class="mokojoomhero__content" style="text-align: <?php echo htmlspecialchars($textAlign, ENT_QUOTES, 'UTF-8'); ?>; color: <?php echo htmlspecialchars($textColor, ENT_QUOTES, 'UTF-8'); ?>;">
<?php if ($heroContent || $module->showtitle) : ?>
<?php if ($showCard) : ?>
<div class="mokojoomhero__card"<?php if ($cardDelay) : ?> style="animation-delay: <?php echo $cardDelay; ?>ms;" data-card-delay="<?php echo $cardDelay; ?>"<?php endif; ?>>
<?php if ($module->showtitle) : ?>
<h2 class="mokojoomhero__title"><?php echo htmlspecialchars($module->title, ENT_QUOTES, 'UTF-8'); ?></h2>
<?php endif; ?>
<?php echo $heroContent; ?>
</div>
<?php else : ?>
<?php if ($module->showtitle) : ?>
<h2 class="mokojoomhero__title"><?php echo htmlspecialchars($module->title, ENT_QUOTES, 'UTF-8'); ?></h2>
<?php endif; ?>
<?php echo $heroContent; ?>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</div>
-35
View File
@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Joomla Extension Update Server XML
See: https://docs.joomla.org/Deploying_an_Update_Server
This file is the update server manifest for mod_mokojoomhero.
The Joomla installer polls this URL to check for new versions.
The manifest in this repository must reference this file:
<updateservers>
<server type="extension" priority="1" name="MokoJoomHero Updates">
https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/updates.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>Module - MokoJoomHero</name>
<description>MokoJoomHero — A Joomla hero image module by Moko Consulting</description>
<element>mod_mokojoomhero</element>
<type>module</type>
<client>site</client>
<version>{{VERSION}}</version>
<downloads>
<downloadurl type="full" format="zip">
https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/v{{VERSION}}/mod_mokojoomhero.zip
</downloadurl>
</downloads>
<targetplatform name="joomla" version="(5|6).*" />
<php_minimum>8.1</php_minimum>
</update>
</updates>
-103
View File
@@ -1,103 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
VERSION: 01.07.00
-->
<updates>
<update>
<name>MokoJoomHero</name>
<description>MokoJoomHero stable build.</description>
<element>mod_mokojoomhero</element>
<type>module</type>
<client>site</client>
<version>01.06.00</version>
<creationDate>2026-05-30</creationDate>
<infourl title="MokoJoomHero">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/stable</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/stable/mod_mokojoomhero-01.06.00.zip</downloadurl>
</downloads>
<sha256>de21e4010e19323746c9aeff12d6a240dc29a2a0c3ef1e091549a2331e710bc7</sha256>
<tags><tag>stable</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*"/>
</update>
<update>
<name>Module - MokoJoomHero</name>
<description>Module - MokoJoomHero dev build.</description>
<element>mod_mokojoomhero</element>
<type>module</type>
<client>site</client>
<version>01.07.00-dev</version>
<creationDate>2026-05-30</creationDate>
<infourl title="Module - MokoJoomHero">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/development</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/development/mod_mokojoomhero-01.07.00-dev.zip</downloadurl>
</downloads>
<sha256>32301fad78e9ad31613d2d02a8f2dd0519f5118b7683335b36532a4e1e5fe969</sha256>
<tags><tag>dev</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*"/>
</update>
<update>
<name>Module - MokoJoomHero</name>
<description>Module - MokoJoomHero alpha build.</description>
<element>mod_mokojoomhero</element>
<type>module</type>
<client>site</client>
<version>01.07.00-alpha</version>
<creationDate>2026-05-30</creationDate>
<infourl title="Module - MokoJoomHero">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/alpha</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/alpha/mod_mokojoomhero-01.07.00-alpha.zip</downloadurl>
</downloads>
<sha256>32301fad78e9ad31613d2d02a8f2dd0519f5118b7683335b36532a4e1e5fe969</sha256>
<tags><tag>alpha</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*"/>
</update>
<update>
<name>Module - MokoJoomHero</name>
<description>Module - MokoJoomHero beta build.</description>
<element>mod_mokojoomhero</element>
<type>module</type>
<client>site</client>
<version>01.07.00-beta</version>
<creationDate>2026-05-30</creationDate>
<infourl title="Module - MokoJoomHero">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/beta</infourl>
<downloads>
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/beta/mod_mokojoomhero-01.07.00-beta.zip</downloadurl>
</downloads>
<sha256>32301fad78e9ad31613d2d02a8f2dd0519f5118b7683335b36532a4e1e5fe969</sha256>
<tags><tag>beta</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*"/>
</update>
<update>
<name>Module - MokoJoomHero</name>
<description>Module - MokoJoomHero rc build.</description>
<element>mod_mokojoomhero</element>
<type>module</type>
<client>site</client>
<version>01.07.00-rc</version>
<creationDate>2026-05-30</creationDate>
<infourl title='Module - MokoJoomHero'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/tag/release-candidate</infourl>
<downloads>
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/releases/download/release-candidate/mod_mokojoomhero-01.07.00-rc.zip</downloadurl>
</downloads>
<sha256>32301fad78e9ad31613d2d02a8f2dd0519f5118b7683335b36532a4e1e5fe969</sha256>
<tags><tag>rc</tag></tags>
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoJoomHero/raw/branch/main/CHANGELOG.md</changelogurl>
<maintainer>Moko Consulting</maintainer>
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
<targetplatform name="joomla" version="(5|6)\..*" />
</update>
</updates>