dev-02.00 to main #1

Merged
mokoconsulting-tech merged 6 commits from dev-02.00 into main 2025-08-30 23:50:41 +00:00
166 changed files with 17565 additions and 1153 deletions

113
.gitignore vendored
View File

@@ -1,65 +1,72 @@
@ -0,0 +1,47 @@ # ============================================================
# Logs and databases
/logs/
/tmp/
/documents/
/administrator/logs/
/packages/*
#Dev related
build/
dev/
scripts/
#Configuration files
/configuration.php
/htdocs/conf/*
# Joomla-generated files
installation/
/cache/
/media/com_joomlaupdate/
# Backup files
*.bak
*.backup
/backups/
/htdocs/solo/
# Environment-specific files
.env .env
.env.local .env.local
.env.*.local .env.*.local
*.local.php
*.secret.php
# Node.js dependencies (if using npm for template development) # ============================================================
node_modules/ # Logs, dumps & databases
npm-debug.log* # ============================================================
*.log
*.pid
*.seed
*.sql
*.sql.gz
*.sqlite
*.sqlite3
*.db
*.db-journal
# Build files # ============================================================
/dist/ # OS / Editor / IDE cruft
/build/ # ============================================================
.DS_Store
Thumbs.db
desktop.ini
# Windows artifacts
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
$RECYCLE.BIN/
System Volume Information/
*.lnk
Icon?
.idea/
.vscode/
*.code-workspace
*.sublime-project
*.sublime-workspace
.project
.settings/
.buildpath
# Compiled assets # ============================================================
# Dev scripts & scratch
# ============================================================
$1TODO
todo
# ============================================================
# Maps & compiled asset helpers
# ============================================================
*.css.map *.css.map
*.js.map *.js.map
# System files # ============================================================
desktop.ini # SFTP / sync tools
.DS_Store # ============================================================
Thumbs.db
.idea/
*.sublime*
sftp-config*.json sftp-config*.json
*.ffs* sync.ffs_db
*.ffs_gui
# Editor and IDE files # ============================================================
.vscode/ # Replit / cloud IDE
*.swp # ============================================================
.replit
replit.md
# Other # ============================================================
*.lock # Keep-empty folders helper
upgrade.unlock # ============================================================
*conf*.php !.gitkeep
*.back*
*.bak*
*.old

85
CHANGELOG.md Normal file
View File

@@ -0,0 +1,85 @@
<!--
=========================================================================
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
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
along with this program. If not, see https://www.gnu.org/licenses/ .
=========================================================================
FILE INFORMATION
DEFGROUP: Joomla
INGROUP: Moko-Cassiopeia
PATH: CHANGELOG.md
VERSION: 02.00
BRIEF: Changelog file documenting version history of Moko-Cassiopeia
=========================================================================
-->
Changelog — Moko-Cassiopeia
# Version 2.0 (2025-08-30)
**Major Release** — introduces the long-awaited **Dark Mode Toggle**, streamlining accessibility and usability enhancements.
## Added
* **Dark Mode Toggle**
* Frontend toggle switch included in template.
* JavaScript handles switching between light/dark modes.
* Dark mode CSS rules applied across template styles.
* Automatic persistence of user choice (via localStorage).
* **Header Parameters Update**
* Added **logo parameter support** in template settings.
* Updated metadata & copyright header.
* **Expanded TOC (Table of Contents)**
* Automatic TOC injection when enabled.
* User selects placement via article > options > layout (`toc-left` or `toc-right`).
## Improved
* Cleaned up `index.php` by removing **skip-to-content** duplicate calls.
* Consolidated JavaScript asset loading (ensuring dark-mode script is loaded correctly from external JS file).
* Streamlined CSS for **toggle switch**, ensuring it inherits Bootstrap/Cassiopeia defaults.
* General accessibility refinements in typography and color contrast.
## Fixed
* Fixed missing **logo param** in header output.
* Corrected stylesheet inconsistencies between Bootstrap 5 helpers and template overrides.
* Patched redundant calls in script includes.
---
#Previous Versions
## 1.0
* **Initial Public Release** with:
* Font Awesome 6
* Bootstrap 5 helpers
* Automatic Table of Contents (TOC) utility
* Moko Expansions: Google Tag Manager / GA4 hooks
---
For the full development roadmap, visit:
[Moko-Cassiopeia Roadmap](https://mokoconsulting.tech/support/joomla-cms/moko-cassiopeia-roadmap)

View File

@@ -1,22 +1,31 @@
<!-- <!--
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech> =========================================================================
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project. This file is part of a Moko Consulting project.
This documentation is free software: you can redistribute it and/or modify SPDX-License-Identifier: GPL-3.0-or-later
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 documentation is distributed in the hope that it will be useful, This program is free software; you can redistribute it and/or modify
but WITHOUT ANY WARRANTY; without even the implied warranty of it under the terms of the GNU General Public License as published by
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the the Free Software Foundation; either version 3 of the License, or
GNU General Public License for more details. (at your option) any later version.
You should have received a copy of the GNU General Public License This program is distributed in the hope that it will be useful,
along with this documentation. If not, see <https://www.gnu.org/licenses/>. 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.
SPDX-License-Identifier: GPL-3.0-or-later You should have received a copy of the GNU General Public License
along with this program. If not, see https://www.gnu.org/licenses/ .
=========================================================================
FILE INFORMATION
DEFGROUP: Joomla
INGROUP: Moko-Cassiopeia
PATH: CODE_OF_CONDUCT.md
VERSION: 02.00
BRIEF: Contributor Code of Conduct for Moko-Cassiopeia project
=========================================================================
--> -->
# Code of Conduct # Code of Conduct

View File

@@ -1,3 +1,33 @@
<!--
=========================================================================
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
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
along with this program. If not, see https://www.gnu.org/licenses/ .
=========================================================================
FILE INFORMATION
DEFGROUP: Joomla
INGROUP: Moko-Cassiopeia
PATH: CONTRIBUTING.md
VERSION: 02.00
BRIEF: Contribution guidelines for the Moko-Cassiopeia project
=========================================================================
-->
# Contributing Guidelines # Contributing Guidelines
Thank you for considering contributing to this project! We welcome contributions from the community and are grateful for your support. Thank you for considering contributing to this project! We welcome contributions from the community and are grateful for your support.
@@ -6,27 +36,27 @@ Thank you for considering contributing to this project! We welcome contributions
1. **Fork the repository** 1. **Fork the repository**
- Click the "Fork" button at the top of the repository page. - Click the "Fork" button at the top of the repository page.
2. **Create a branch** 2. **Create a branch**
```bash ```bash
git checkout -b feature/your-feature-name git checkout -b feature/your-feature-name
``` ```
3. **Make your changes** 3. **Make your changes**
- Follow the existing code style. - Follow the existing code style.
- Write clear commit messages. - Write clear commit messages.
4. **Test your changes** 4. **Test your changes**
- Ensure all tests pass and new code is covered. - Ensure all tests pass and new code is covered.
5. **Submit a pull request (PR)** 5. **Submit a pull request (PR)**
- Push your branch to your fork. - Push your branch to your fork.
- Open a PR against the `main` branch. - Open a PR against the `main` branch.
--- ---

444
README.md
View File

@@ -1,254 +1,304 @@
# Moko-Cassiopeia <!--
=========================================================================
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
A modern, lightweight enhancement layer for Joomlas Cassiopeia template. Moko-Cassiopeia adds **Font Awesome 6**, **Bootstrap 5** helpers, an automatic **Table of Contents (TOC)** utility, and optional **Moko Expansions** including **Google Tag Manager** and **Google Analytics (GA4)** hooks—all while keeping core template overrides minimal and upgradefriendly. This file is part of a Moko Consulting project.
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
along with this program. If not, see https://www.gnu.org/licenses/ .
=========================================================================
FILE INFORMATION
DEFGROUP: Joomla
INGROUP: Moko-Cassiopeia
PATH: README.md
VERSION: 02.00
BRIEF: Project readme for Moko-Cassiopeia, including features, setup, and usage
=========================================================================
-->
# Moko-Cassiopeia v02.00 — README
> Joomla! site template by **Moko Consulting**
> License: **GPL-3.0-or-later**
> Compatibility: **Joomla 4.4+ / 5.x** (PHP 8.1+)
--- ---
## Table of Contents ## Overview
- [Features](#features) Moko-Cassiopeia is a streamlined, Bootstrap-friendly Joomla template with a tokenized color system, Google Tag Manager / Analytics hooks, and performance-minded assets.
- [Requirements](#requirements)
- [Quick Start](#quick-start) * **v02.00 (2025-08-30)** introduces **Dark Mode** with OS auto-detection **and** an optional **Dark Mode Toggle** (ID **25**) so users can manually switch themes; their preference persists.
- [Installation](#installation) * **v01.00** was the initial public release (FA6, BS5, TOC, GTM/GA hooks).
- [Configuration](#configuration)
- [Global Params](#global-params) Public roadmap: **[https://mokoconsulting.tech/support/joomla-cms/moko-cassiopeia-roadmap](https://mokoconsulting.tech/support/joomla-cms/moko-cassiopeia-roadmap)**
- [Font Awesome 6](#font-awesome-6)
- [Bootstrap 5 Helpers](#bootstrap-5-helpers) ---
- [TOC Function](#toc-function)
- [Moko Expansions](#moko-expansions) ## Whats New in v02.00
- [Google Tag Manager](#google-tag-manager)
- [Google Analytics (GA4)](#google-analytics-ga4) * **Dark Mode** with `prefers-color-scheme` auto-detect.
- [CSS Variables](#css-variables) * **Dark Mode Toggle** (Template param **ID 25**) with positions **Header / Navbar / Footer**.
- [Usage Examples](#usage-examples)
- [Best Practices](#best-practices) * Persists choice to `localStorage` (key: `moko.theme`).
- [Development](#development) * Keyboard- and screen-reader-friendly; focus ring uses theme tokens.
- [Changelog](#changelog) * Admin option **“Show Theme Toggle”** (`On/Off`).
- [License](#license) * CSS refactor to **variables**: light tokens in `:root`, dark overrides in `[data-theme="dark"]`.
* Lowered CSS specificity, `rem` units for scalable typography/spacing.
* Stabilized WebAsset registrations (LTR/RTL presets).
--- ---
## Features ## Features
- **Font Awesome 6**: Solid, Regular, Brands (locally enqueued or CDN with Subresource Integrity). * **Dark Mode + Toggle**
- **Bootstrap 5**: Utility classes, grid, and components available to your layouts and modules. Auto-detect plus manual switch; persistent per user.
- **Auto TOC**: Generate an inpage Table of Contents from headings with a single data attribute.
- **Moko Expansions**: * **Bootstrap-friendly CSS**
- **GTM**: Dropin dataLayer and container injection with a template param. Low specificity, variable-driven utilities for buttons, alerts, typography, spacing.
- **GA4**: Native GA4 Measurement ID support (with or without GTM).
- **Productionsafe**: Assets loaded conditionally; no duplicate library loads if another extension already enqueues them. * **GTM / GA Hooks**
- **Accessible by default**: TOC anchors and focus styles follow a11y guidelines. Clean injection points for Google Tag Manager and optional direct GA4 tag.
* **LTR / RTL Presets**
Stable asset registration pattern for palette and template styles.
* **A11y & Performance**
Clear focus styling and balanced contrast; minified bundles in production.
---
## Requirements ## Requirements
- Joomla 4.4+ or Joomla 5+ * Joomla **4.4+** / **5.x**
- PHP 8.1+ * PHP **8.1+**
- Cassiopeia as the active base template (Moko-Cassiopeia may be installed as a child/override set) * Modern evergreen browsers (graceful fallback if `prefers-color-scheme` isnt available)
## Quick Start ---
1. Install the template package via Joomla Extension Manager.
2. In the Template Style settings, enable the features you want (FA6, BS5, TOC, GTM/GA).
3. (Optional) Add a TOC container to any article or module using the data attribute shown below.
## Installation ## Installation
1. Go to 'System''Install' → 'Extensions'. 1. **Install** via *Extensions → Manage → Install* (upload the template `.zip`).
2. Upload 'Moko-Cassiopeia.zip'. 2. **Set as default** in *System → Templates → Site Templates*.
3. After installation, go to 'System''Site Templates' → 'Styles' and open 'Moko-Cassiopeia'. 3. **Clear caches**: *System → Clear Cache* and hard-reload your browser.
4. Choose 'Default' to make it your active style (or assign per menu item).
## Configuration ---
Configuration lives in the Template Style parameters. Common params are listed below. Names may vary slightly depending on release. ## Template Options (high-level)
### Global Params **Theme**
- 'load\_bootstrap5' (bool): Enqueue Bootstrap 5 core CSS/JS if not provided by Joomla context. * **Force Theme**: `Auto` (default) | `Light` | `Dark`
- 'load\_fontawesome6' (bool): Enqueue Font Awesome 6 (solid, regular, brands). * **Show Theme Toggle** (ID **25**): `On` | `Off`
- 'use\_cdn' (bool): Use CDN with SRI instead of local assets. * **Toggle Position**: `Header` | `Navbar` | `Footer`
- 'minified' (bool): Prefer '.min' assets. * **Default Theme** (when not using Auto): `Light`
- 'defer\_js' (bool): Add 'defer' to templateinjected scripts where safe.
### Font Awesome 6 **GTM / Analytics**
When enabled, the template registers FA6 and exposes the standard icon syntax: * **GTM Container ID** (e.g., `GTM-XXXXXXX`)
* **Analytics Tag ID** (optional GA4 if not using GTM)
```html **Performance**
<i class='fa-solid fa-circle-check' aria-hidden='true'></i>
<span class='visually-hidden'>Success</span>
```
**Notes** * **Development Mode**
- Icons are decorative unless paired with text or 'aria-label'. * `Off``.min.css` / `.min.js` bundles
- Prefer the 'fa-solid', 'fa-regular', or 'fa-brands' families explicitly. * `On` → unminified sources for debugging
### Bootstrap 5 Helpers ---
If 'load\_bootstrap5' is enabled, grid and utilities are available: ## Dark Mode — Tokens & Toggle
```html **Color tokens**
<div class='container'>
<div class='row g-3'>
<div class='col-12 col-md-6'>Left</div>
<div class='col-12 col-md-6'>Right</div>
</div>
</div>
```
You can also use helpers like 'ratio', 'visually-hidden', 'd-flex', and spacing utilities (e.g., 'mt-3', 'px-4').
### TOC Function
Moko-Cassiopeia ships a tiny script that scans within a container for headings (h2h6) and builds a nested TOC with anchor links.
**Enable**: Turn on 'auto\_toc' in Template Style.
**Place a TOC container**:
```html
<nav class='toc' data-moko-toc='true' data-moko-toc-target='#article-body' aria-label='Table of contents'></nav>
```
**Mark your content region**:
```html
<article id='article-body'>
<h2>Section A</h2>
<p>...</p>
<h3>Subsection A.1</h3>
<p>...</p>
</article>
```
**Options via data attributes**
- 'data-moko-toc-target': CSS selector for the content area (default: 'main').
- 'data-moko-toc-levels': CSV or range string like '2-4' (default: '2-4').
- 'data-moko-toc-collapsible': 'true'|'false' to make nested lists collapsible.
**Styling**
A minimal stylesheet is included. Customize using the CSS variables below or add your own overrides.
### Moko Expansions
#### Google Tag Manager
Enable GTM by entering your container ID (e.g., 'GTM-XXXXXXX') in Template Style under 'Moko Expansions'. The template will inject the standard script and 'noscript' iframe per Google guidance.
**Data Layer**
You can push events from modules or overrides like so:
```html
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'moko_event',
'moko_category': 'ui',
'moko_action': 'toc_opened',
'moko_label': 'sidebar'
});
</script>
```
#### Google Analytics (GA4)
Two options:
1. **Direct GA4**: Provide 'G-' Measurement ID (e.g., 'G-ABC123XYZ'). The template injects the GA4 base script.
2. **Via GTM**: Leave GA4 field empty and configure GA4 inside your GTM container.
```html
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ 'event': 'page_view' });
</script>
```
> Tip: When both GTM and direct GA4 are set, the template prefers GTM to avoid duplicate pageviews.
## CSS Variables
Moko-Cassiopeia exposes custom properties for theme tuning. Example set:
```css ```css
:root { :root {
--moko-cassiopeia-color-primary: #0b4008; --color-bg: #ffffff;
--moko-cassiopeia-color-link: #0b4008; --color-surface: #f8f9fa;
--moko-cassiopeia-color-hover: #000000; --color-text: #1d2125;
--color-text-muted: #6c757d;
--color-border: #dee2e6;
--moko-cassiopeia-header-background-image: linear-gradient(30deg, #fefcf9, var(--accent-color-primary)); --color-primary: #0d6efd;
--moko-cassiopeia-header-background-position: auto; --color-primary-contrast: #ffffff;
--moko-cassiopeia-header-background-attachment: fixed;
--moko-cassiopeia-header-background-repeat: repeat; --color-link: var(--color-primary);
--moko-cassiopeia-header-background-size: auto; --color-link-hover: #0b5ed7;
--focus-ring: 0 0 0 .2rem rgba(13,110,253,.25);
}
[data-theme="dark"] {
--color-bg: #0e1116;
--color-surface: #151922;
--color-text: #e7eaf0;
--color-text-muted: #a4acb9;
--color-border: #2a3240;
--color-primary: #66b2ff;
--color-primary-contrast: #0d1117;
--color-link: var(--color-primary);
--color-link-hover: #99ccff;
--focus-ring: 0 0 0 .2rem rgba(102,178,255,.35);
} }
``` ```
> Apply these in a custom stylesheet or template options if provided. Use semantic variables where possible to maintain consistency. **Programmatic switch (optional)**
## Usage Examples ```js
// Apply and persist a choice
### 1) FA6 Icon Buttons document.documentElement.dataset.theme = 'dark'; // or 'light'
localStorage.setItem('moko.theme', 'dark'); // namespaced key
```html
<a class='btn btn-primary d-inline-flex align-items-center' href='#'>
<i class='fa-solid fa-bolt me-2' aria-hidden='true'></i>
<span>Action</span>
</a>
``` ```
### 2) Sticky Sidebar TOC ---
```html ## CSS Architecture
<aside class='position-sticky top-0 pt-3'>
<nav class='toc' data-moko-toc='true' data-moko-toc-target='#content'></nav> * **`template.css`** = structure/layout and component scaffolding
</aside> * **No hard-coded hex** in core selectors; all colors reference tokens
* **Units**: `rem` (replacing `em`) for scalable typography/spacing
* **Low specificity** to play nicely with Bootstrap and content plugins
---
## GTM / Analytics Integration
* Enter **GTM Container ID** in Template Options to inject the GTM snippet.
* Optionally add a **GA4 Measurement ID** if not routing GA via GTM.
* Output uses Joomla rendering events to avoid duplication.
> Verify tags with DevTools / Tag Assistant.
---
## RTL / LTR Assets (WebAsset JSON)
Minimal pattern:
```json
{
"$schema": "https://developer.joomla.org/schemas/json/schema_web_assets.json",
"name": "template.moko-cassiopeia",
"assets": [
{ "name": "template.moko-cassiopeia.styles", "type": "style", "uri": "templates/moko-cassiopeia/css/template.min.css" },
{ "name": "template.moko-cassiopeia.palette", "type": "style", "uri": "templates/moko-cassiopeia/css/colors_standard.min.css" },
{ "name": "template.moko-cassiopeia", "type": "preset", "dependencies": ["template.moko-cassiopeia.styles","template.moko-cassiopeia.palette"] }
]
}
``` ```
### 3) Moduledriven GA Event In `index.php`:
```php ```php
<?php /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
// In a custom module layout $wa = $this->getWebAssetManager();
$label = 'cta_hero'; $wa->usePreset('template.moko-cassiopeia');
?>
<button class='btn btn-outline-primary' onclick='window.dataLayer && window.dataLayer.push({"event": "cta_click", "label": "<?php echo $label; ?>"})'>
Click me
</button>
``` ```
> Note: We use single quotes in HTML where possible to keep consistency with PHP string style preferences. ---
## Best Practices ## Upgrade Notes
- **One source of truth** for analytics injection (prefer GTM, or direct GA4—not both). **1.0 → 2.0**
- **Defer noncritical JS** using the 'defer\_js' param when feasible.
- **Avoid duplicate libraries** if another extension already loads FA6/BS5.
- **Respect a11y**: Provide visible focus, 'visually-hidden' labels, and heading order for the TOC.
- **Cache smartly**: After enabling new features, clear Joomla cache and any CDN cache to propagate assets.
## Development * Clear Joomla + browser caches.
* Convert any hard-coded colors in overrides to **tokens** (e.g., `var(--color-text-muted)`).
* Review spacing/typography where `rem` replaces `em`.
* Verify asset names if you referenced WebAsset handles directly.
* If you previously added a custom dark-mode toggle, remove it and enable **Show Theme Toggle** (ID **25**).
- Source structure follows Joomla template conventions: ---
- '/css', '/js', '/images', '/html' (overrides), 'templateDetails.xml'
- Scripts are enqueued via the template's 'index.php' with conditional params.
- Build/compile steps (if using bundlers) are noted in 'package.json' (when applicable).
**Local overrides** ## Accessibility
- Place sitespecific CSS in '/css/custom.css'. * Improved contrast targets across light/dark.
- Use '/html' for component/module layout overrides as needed. * Visible, consistent focus indicators.
* Toggle is keyboard-navigable and labeled for assistive tech.
## Changelog ---
- 1.15: Added CSS theme seletor (dark/light) ## Troubleshooting
- 1.00: Initial public release with FA6, BS5, TOC, GTM/GA hooks.
## License * **Toggle not visible** → Ensure “Show Theme Toggle” is on and placed in a visible position.
* **Preference not persisting** → Check `localStorage` availability and console for JS errors.
* **Asset dependency warnings** → Confirm preset/asset names match your `joomla.asset.json`.
Distributed under the GNU General Public License v3. See 'LICENSE' for details. ---
## Feature Rundown & Comparison
### Moko-Cassiopeia v01.00 — Initial public release
* **Font Awesome 6** integrated; **Bootstrap 5** helpers.
* **TOC utility** hooks for article table of contents.
* **GTM/GA hooks** with safe injection points.
* Minimal, upgrade-friendly overrides; variable-ready CSS.
### Moko-Cassiopeia v02.00 — Dark Mode + Toggle (ID 25)
* **Dark Mode** with OS auto-detect.
* **Optional Dark Mode Toggle** (ID 25) in Header / Navbar / Footer; persisted per user.
* **Tokenized palette** (`:root` + `[data-theme="dark"]`).
* **CSS refactor**: low specificity; `rem` units; Bootstrap-friendly utilities.
* Stabilized **Web Asset** registrations (LTR/RTL presets).
### Baseline: Cassiopeia (Joomla 4.4 / 5.x)
* Responsive, accessible core site template with Bootstrap-friendly markup.
* Template options for color preset, layout width, sticky header, and module menu layouts.
* Web Asset Manager integration (`joomla.asset.json`, `$this->getWebAssetManager()`).
---
## Roadmap
Public roadmap: **[https://mokoconsulting.tech/support/joomla-cms/moko-cassiopeia-roadmap](https://mokoconsulting.tech/support/joomla-cms/moko-cassiopeia-roadmap)**
---
## Changelog (1.0 → 2.0)
### 02.00 — 2025-08-30 — “Dark Mode”
**Added**
* Dark Mode with OS auto-detection (`prefers-color-scheme`).
* **Dark Mode Toggle** (param **ID 25**) with positions Header / Navbar / Footer; persists choice via `localStorage` (`moko.theme`); accessible markup and focus styling.
* Tokenized CSS palette with `[data-theme="dark"]` overrides.
* Admin override to force Light/Dark/Auto.
* Bootstrap-friendly utility hooks mapped to tokens.
**Changed**
* `template.css` now structure/layout only; colors via tokens.
* `em``rem`; reduced specificity; standardized focus indicators.
**Fixed**
* WebAsset registrations (LTR/RTL/preset deps) and dark-theme link/muted contrast.
**Removed / Deprecated**
* Hard-coded color declarations and legacy hex-based helper classes.
---
### 01.00 — Initial public release
* **FA6**, **BS5**, **TOC**, **GTM/GA** hooks.

View File

@@ -1,13 +1,30 @@
;--------------------------------------------------- # =========================================================================
; Template: moko-cassiopeia # Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
; File: en-GB.tpl_moko-cassiopeia.ini #
; Version: 02.00 # This file is part of a Moko Consulting project.
; Author: Jonathan Miller #
; Copyright: (C) 2025 Moko Consulting. All rights reserved. # SPDX-License-Identifier: GPL-3.0-or-later
; License: GNU General Public License v3 or later; see LICENSE.txt #
; Description: Language strings for the frontend template. # This program is free software; you can redistribute it and/or modify
; Note: All ini files must be saved as UTF-8 without BOM. # 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
# along with this program. If not, see https://www.gnu.org/licenses/ .
# =========================================================================
# FILE INFORMATION
# DEFGROUP: Joomla
# INGROUP: Moko-Cassiopeia
# PATH: language/en-GB/tpl_moko-cassiopeia.ini
# VERSION: 02.00
# BRIEF: English (GB) language strings for the Moko-Cassiopeia Joomla template
# =========================================================================
; ===== Template meta ===== ; ===== Template meta =====
MOKO-CASSIOPEIA="MOKO-CASSIOPEIA Site template" MOKO-CASSIOPEIA="MOKO-CASSIOPEIA Site template"

View File

@@ -1,13 +1,30 @@
;--------------------------------------------------- # =========================================================================
; Template: moko-cassiopeia # Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
; File: en-GB.tpl_moko-cassiopeia.sys.ini #
; Version: 02.00 # This file is part of a Moko Consulting project.
; Author: Jonathan Miller #
; Copyright: (C) 2025 Moko Consulting. All rights reserved. # SPDX-License-Identifier: GPL-3.0-or-later
; License: GNU General Public License v3 or later; see LICENSE.txt #
; Description: Language strings for the frontend template. # This program is free software; you can redistribute it and/or modify
; Note: All ini files must be saved as UTF-8 without BOM. # 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
# along with this program. If not, see https://www.gnu.org/licenses/ .
# =========================================================================
# FILE INFORMATION
# DEFGROUP: Joomla
# INGROUP: Moko-Cassiopeia
# PATH: language/en-GB/tpl_moko-cassiopeia.sys.ini
# VERSION: 02.00
# BRIEF: English (GB) system language strings for template metadata and installer
# =========================================================================
TPL_MOKO-CASSIOPEIA="Moko-Cassiopeia Site template" TPL_MOKO-CASSIOPEIA="Moko-Cassiopeia Site template"
TPL_MOKO-CASSIOPEIA_MOD_MENU_LAYOUT_COLLAPSE-METISMENU="Collapsible Dropdown" TPL_MOKO-CASSIOPEIA_MOD_MENU_LAYOUT_COLLAPSE-METISMENU="Collapsible Dropdown"
@@ -30,11 +47,12 @@ TPL_MOKO-CASSIOPEIA_POSITION_TOP-B="Top-b"
TPL_MOKO-CASSIOPEIA_POSITION_TOPBAR="Top Bar" TPL_MOKO-CASSIOPEIA_POSITION_TOPBAR="Top Bar"
TPL_MOKO-CASSIOPEIA_POSITION_DRAWER-LEFT="Drawer-Left" TPL_MOKO-CASSIOPEIA_POSITION_DRAWER-LEFT="Drawer-Left"
TPL_MOKO-CASSIOPEIA_POSITION_DRAWER-RIGHT="Drawer-Right" TPL_MOKO-CASSIOPEIA_POSITION_DRAWER-RIGHT="Drawer-Right"
TPL_MOKO-CASSIOPEIA_XML_DESCRIPTION="<h3>MOKO-CASSIOPEIA Template Description</h3> TPL_MOKO-CASSIOPEIA_XML_DESCRIPTION=
TPL_MOKO-CASSIOPEIA_XML_DESCRIPTION="<h3>MOKO-CASSIOPEIA Template Description (v2.0)</h3>
<p> <p>
<strong>MOKO-CASSIOPEIA</strong> continues Joomlas tradition of space-themed default templates— <strong>MOKO-CASSIOPEIA 2.0</strong> continues Joomlas tradition of space-themed default templates—
building on the legacy of <em>Solarflare</em> from Joomla 1.0, <em>Milkyway</em> from Joomla 1.5, building on the legacy of <em>Solarflare</em> (Joomla 1.0), <em>Milkyway</em> (Joomla 1.5),
and <em>Protostar</em> from Joomla 3.0. and <em>Protostar</em> (Joomla 3.0).
</p> </p>
<p> <p>
This template is a customized fork of the <strong>Cassiopeia</strong> template introduced in Joomla 4, This template is a customized fork of the <strong>Cassiopeia</strong> template introduced in Joomla 4,
@@ -42,41 +60,40 @@ TPL_MOKO-CASSIOPEIA_XML_DESCRIPTION="<h3>MOKO-CASSIOPEIA Template Description</h
enhancements and structural refinements specifically tailored for use by Moko Consulting. enhancements and structural refinements specifically tailored for use by Moko Consulting.
</p> </p>
<p> <p>
<strong>MOKO-CASSIOPEIA</strong> is designed to serve as a versatile, production-ready base for <strong>Version 2.0</strong> introduces significant new functionality including a Dark Mode toggle,
contemporary Joomla websites, emphasizing speed, clarity, and open-source philosophy. Google Tag Manager (GTM) and Google Analytics 4 (GA4) hooks, and expanded template configuration
options — all while keeping overrides minimal and upgrade-friendly.
</p> </p>
<h4>Features</h4> <h4>Features</h4>
<ul> <ul>
<li>Fully responsive and mobile-first layout</li> <li>Fully responsive and mobile-first layout</li>
<li>Based on Joomla 4+ template architecture</li> <li>Based on Joomla 4+ template architecture</li>
<li>Enhanced SCSS and CSS overrides for custom styling</li> <li>Enhanced SCSS and CSS overrides for streamlined custom styling</li>
<li>Built-in support for Bootstrap 5</li> <li>Built-in support for <strong>Bootstrap 5</strong></li>
<li> <li>Font Awesome 6 integration for modern iconography</li>
Integrated dynamic Table of Contents via <li>Automatic Table of Contents (TOC) — selectable per article via <code>toc-left</code> or <code>toc-right</code> layouts</li>
<a href="https://afeld.github.io/bootstrap-toc/" target="_blank" rel="noopener">Bootstrap TOC</a> <li><strong>Dark Mode toggle (new in v2.0)</strong> with user switch and admin override</li>
</li> <li><strong>Optional GTM + GA4 hooks (new in v2.0)</strong> for analytics and marketing integration</li>
<li>Optimized template structure for performance and maintainability</li> <li>Optimized template structure for performance and maintainability</li>
<li>Custom module positions and layout presets</li> <li>Custom module positions and layout presets</li>
<li>Accessible, lightweight, and extensible</li> <li>Accessible, lightweight, and extensible for long-term use</li>
<li>Ideal for professional services, portfolios, and informational websites</li> <li>Ideal for professional services, portfolios, and informational websites</li>
</ul> </ul>
<h4>Code Attribution</h4> <h4>Code Attribution</h4>
<p> <p>
This template is based on the original <strong>Cassiopeia</strong> template developed by the This template is based on the original <strong>Cassiopeia</strong> template developed by the
<a href="https://www.joomla.org" target="_blank" rel="noopener">Joomla! Project</a> and released under the GNU General Public License. <a href=\"https://www.joomla.org\" target=\"_blank\" rel=\"noopener\">Joomla! Project</a> and released under the GNU General Public License.
</p> </p>
<p> <p>
Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards. Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.
</p>a </p>
<p> <p>
It includes integration with It includes integration with
<a href="https://afeld.github.io/bootstrap-toc/" target="_blank" rel="noopener">Bootstrap TOC</a>, <a href=\"https://afeld.github.io/bootstrap-toc/\" target=\"_blank\" rel=\"noopener\">Bootstrap TOC</a>,
an open-source table of contents generator by A. Feld, licensed under the MIT License. an open-source table of contents generator by A. Feld, licensed under the MIT License.
</p> </p>
<p> <p>
All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable. All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.
</p>" </p>"
JGLOBAL_OFFLINE="Offline"

View File

@@ -1,13 +1,30 @@
;--------------------------------------------------- # =========================================================================
; Template: moko-cassiopeia # Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
; File: en-GB.tpl_moko-cassiopeia.ini #
; Version: 02.00 # This file is part of a Moko Consulting project.
; Author: Jonathan Miller #
; Copyright: (C) 2025 Moko Consulting. All rights reserved. # SPDX-License-Identifier: GPL-3.0-or-later
; License: GNU General Public License v3 or later; see LICENSE.txt #
; Description: Language strings for the frontend template. # This program is free software; you can redistribute it and/or modify
; Note: All ini files must be saved as UTF-8 without BOM. # 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
# along with this program. If not, see https://www.gnu.org/licenses/ .
# =========================================================================
# FILE INFORMATION
# DEFGROUP: Joomla
# INGROUP: Moko-Cassiopeia
# PATH: language/en-US/tpl_moko-cassiopeia.ini
# VERSION: 02.00
# BRIEF: English (US) language strings for the Moko-Cassiopeia Joomla template
# =========================================================================
; ===== Template meta ===== ; ===== Template meta =====
MOKO-CASSIOPEIA="MOKO-CASSIOPEIA Site template" MOKO-CASSIOPEIA="MOKO-CASSIOPEIA Site template"

View File

@@ -1,13 +1,30 @@
;--------------------------------------------------- # =========================================================================
; Template: moko-cassiopeia # Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
; File: en-US.tpl_moko-cassiopeia.sys.ini #
; Version: 02.00 # This file is part of a Moko Consulting project.
; Author: Jonathan Miller #
; Copyright: (C) 2025 Moko Consulting. All rights reserved. # SPDX-License-Identifier: GPL-3.0-or-later
; License: GNU General Public License v3 or later; see LICENSE.txt #
; Description: Language strings for the frontend template. # This program is free software; you can redistribute it and/or modify
; Note: All ini files must be saved as UTF-8 without BOM. # 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
# along with this program. If not, see https://www.gnu.org/licenses/ .
# =========================================================================
# FILE INFORMATION
# DEFGROUP: Joomla
# INGROUP: Moko-Cassiopeia
# PATH: language/en-US/tpl_moko-cassiopeia.sys.ini
# VERSION: 02.00
# BRIEF: English (US) system language strings for template metadata and installer
# =========================================================================
TPL_MOKO-CASSIOPEIA="Moko-Cassiopeia Site template" TPL_MOKO-CASSIOPEIA="Moko-Cassiopeia Site template"
TPL_MOKO-CASSIOPEIA_MOD_MENU_LAYOUT_COLLAPSE-METISMENU="Collapsible Dropdown" TPL_MOKO-CASSIOPEIA_MOD_MENU_LAYOUT_COLLAPSE-METISMENU="Collapsible Dropdown"
@@ -31,11 +48,11 @@ TPL_MOKO-CASSIOPEIA_POSITION_TOPBAR="Top Bar"
TPL_MOKO-CASSIOPEIA_POSITION_DRAWER-LEFT="Drawer-Left" TPL_MOKO-CASSIOPEIA_POSITION_DRAWER-LEFT="Drawer-Left"
TPL_MOKO-CASSIOPEIA_POSITION_DRAWER-RIGHT="Drawer-Right" TPL_MOKO-CASSIOPEIA_POSITION_DRAWER-RIGHT="Drawer-Right"
TPL_MOKO-CASSIOPEIA_XML_DESCRIPTION= TPL_MOKO-CASSIOPEIA_XML_DESCRIPTION=
TPL_MOKO-CASSIOPEIA_XML_DESCRIPTION="<h3>MOKO-CASSIOPEIA Template Description</h3> TPL_MOKO-CASSIOPEIA_XML_DESCRIPTION="<h3>MOKO-CASSIOPEIA Template Description (v2.0)</h3>
<p> <p>
<strong>MOKO-CASSIOPEIA</strong> continues Joomlas tradition of space-themed default templates— <strong>MOKO-CASSIOPEIA 2.0</strong> continues Joomlas tradition of space-themed default templates—
building on the legacy of <em>Solarflare</em> from Joomla 1.0, <em>Milkyway</em> from Joomla 1.5, building on the legacy of <em>Solarflare</em> (Joomla 1.0), <em>Milkyway</em> (Joomla 1.5),
and <em>Protostar</em> from Joomla 3.0. and <em>Protostar</em> (Joomla 3.0).
</p> </p>
<p> <p>
This template is a customized fork of the <strong>Cassiopeia</strong> template introduced in Joomla 4, This template is a customized fork of the <strong>Cassiopeia</strong> template introduced in Joomla 4,
@@ -43,41 +60,40 @@ TPL_MOKO-CASSIOPEIA_XML_DESCRIPTION="<h3>MOKO-CASSIOPEIA Template Description</h
enhancements and structural refinements specifically tailored for use by Moko Consulting. enhancements and structural refinements specifically tailored for use by Moko Consulting.
</p> </p>
<p> <p>
<strong>MOKO-CASSIOPEIA</strong> is designed to serve as a versatile, production-ready base for <strong>Version 2.0</strong> introduces significant new functionality including a Dark Mode toggle,
contemporary Joomla websites, emphasizing speed, clarity, and open-source philosophy. Google Tag Manager (GTM) and Google Analytics 4 (GA4) hooks, and expanded template configuration
options — all while keeping overrides minimal and upgrade-friendly.
</p> </p>
<h4>Features</h4> <h4>Features</h4>
<ul> <ul>
<li>Fully responsive and mobile-first layout</li> <li>Fully responsive and mobile-first layout</li>
<li>Based on Joomla 4+ template architecture</li> <li>Based on Joomla 4+ template architecture</li>
<li>Enhanced SCSS and CSS overrides for custom styling</li> <li>Enhanced SCSS and CSS overrides for streamlined custom styling</li>
<li>Built-in support for Bootstrap 5</li> <li>Built-in support for <strong>Bootstrap 5</strong></li>
<li> <li>Font Awesome 6 integration for modern iconography</li>
Integrated dynamic Table of Contents via <li>Automatic Table of Contents (TOC) — selectable per article via <code>toc-left</code> or <code>toc-right</code> layouts</li>
<a href="https://afeld.github.io/bootstrap-toc/" target="_blank" rel="noopener">Bootstrap TOC</a> <li><strong>Dark Mode toggle (new in v2.0)</strong> with user switch and admin override</li>
</li> <li><strong>Optional GTM + GA4 hooks (new in v2.0)</strong> for analytics and marketing integration</li>
<li>Optimized template structure for performance and maintainability</li> <li>Optimized template structure for performance and maintainability</li>
<li>Custom module positions and layout presets</li> <li>Custom module positions and layout presets</li>
<li>Accessible, lightweight, and extensible</li> <li>Accessible, lightweight, and extensible for long-term use</li>
<li>Ideal for professional services, portfolios, and informational websites</li> <li>Ideal for professional services, portfolios, and informational websites</li>
</ul> </ul>
<h4>Code Attribution</h4> <h4>Code Attribution</h4>
<p> <p>
This template is based on the original <strong>Cassiopeia</strong> template developed by the This template is based on the original <strong>Cassiopeia</strong> template developed by the
<a href="https://www.joomla.org" target="_blank" rel="noopener">Joomla! Project</a> and released under the GNU General Public License. <a href=\"https://www.joomla.org\" target=\"_blank\" rel=\"noopener\">Joomla! Project</a> and released under the GNU General Public License.
</p> </p>
<p> <p>
Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards. Modifications and enhancements have been made by Moko Consulting in accordance with open-source licensing standards.
</p>a </p>
<p> <p>
It includes integration with It includes integration with
<a href="https://afeld.github.io/bootstrap-toc/" target="_blank" rel="noopener">Bootstrap TOC</a>, <a href=\"https://afeld.github.io/bootstrap-toc/\" target=\"_blank\" rel=\"noopener\">Bootstrap TOC</a>,
an open-source table of contents generator by A. Feld, licensed under the MIT License. an open-source table of contents generator by A. Feld, licensed under the MIT License.
</p> </p>
<p> <p>
All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable. All third-party libraries and assets remain the property of their respective authors and are credited within their source files where applicable.
</p>" </p>"
JGLOBAL_OFFLINE="Offline"

View File

@@ -1,3 +1,33 @@
@charset "UTF-8";
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/editor.css
* VERSION: 02.00
* BRIEF: Stylesheet for Joomla editor content within Moko-Cassiopeia template
* =========================================================================
*/
/* STYLES FOR JOOMLA! EDITOR */ /* STYLES FOR JOOMLA! EDITOR */
body { body {
font-size: 1rem; font-size: 1rem;

View File

@@ -1,3 +1,33 @@
@charset "UTF-8";
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/gable.css
* VERSION: 02.00
* BRIEF: Stylesheet providing gable-specific layout and design rules for Moko-Cassiopeia
* =========================================================================
*/
:root { :root {
--gab-blue: transparent; --gab-blue: transparent;
--gab-green: #7ac143; --gab-green: #7ac143;

View File

@@ -0,0 +1,368 @@
@charset "UTF-8";
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/global/dark/colors_alternative.css
* VERSION: 02.00
* BRIEF: Alternative dark mode color definitions for Moko-Cassiopeia template
* =========================================================================
*/
/* -----------------------------------------------
* DARK THEME
* --------------------------------------------- */
:root[data-bs-theme='dark']{
/* System hint for native widgets */
color-scheme: dark;
/* Brand & links */
--color-primary: #112855;
--accent-color-primary: #3f8ff0;
--accent-color-secondary: #6fb3ff;
--mainmenu-nav-link-color: #fff;
--color-link: #224FAA;
--color-hover: #224FAA;
/* Header background (kept same image; works over dark bg) */
--header-background-image: url('../../../../../../media/templates/site/moko-cassiopeia/images/bg.svg'); --header-background-attachment: fixed;
--header-background-repeat: repeat;
--header-background-size: auto;
/* Section containers */
--container-below-topbar-bg-image: ;
--container-below-topbar-bg-color: ;
--container-below-topbar-bg-position: center;
--container-below-topbar-bg-attachment: fixed;
--container-below-topbar-bg-repeat: no-repeat;
--container-below-topbar-bg-size: cover;
--container-below-topbar-border: ;
--container-below-topbar-border-radius: ;
--container-top-a-bg-image: ;
--container-top-a-bg-color: ;
--container-top-a-bg-position: center;
--container-top-a-bg-attachment: fixed;
--container-top-a-bg-repeat: no-repeat;
--container-top-a-bg-size: cover;
--container-top-a-border: ;
--container-top-a-border-radius: ;
--container-top-b-bg-image: ;
--container-top-b-bg-color: ;
--container-top-b-bg-position: center;
--container-top-b-bg-attachment: fixed;
--container-top-b-bg-repeat: no-repeat;
--container-top-b-bg-size: cover;
--container-top-b-border: ;
--container-top-b-border-radius: ;
--container-toc-bg: ;
--container-toc-color: #dbe3ff;
--container-sidebar-bg-image: ;
--container-sidebar-bg-color: ;
--container-sidebar-bg-position: center;
--container-sidebar-bg-attachment: scroll;
--container-sidebar-bg-repeat: repeat;
--container-sidebar-bg-size: auto;
--container-sidebar-border: ;
--container-sidebar-border-radius: ;
--container-bottom-a-bg-image: ;
--container-bottom-a-bg-color: ;
--container-bottom-a-bg-position: center;
--container-bottom-a-bg-attachment: fixed;
--container-bottom-a-bg-repeat: no-repeat;
--container-bottom-a-bg-size: cover;
--container-bottom-a-border: ;
--container-bottom-a-border-radius: 5px;
--container-bottom-b-bg-image: ;
--container-bottom-b-bg-color: ;
--container-bottom-b-bg-position: center;
--container-bottom-b-bg-attachment: fixed;
--container-bottom-b-bg-repeat: no-repeat;
--container-bottom-b-bg-size: cover;
--container-bottom-b-border: ;
--container-bottom-b-border-radius: ;
/* Nav & accents */
--nav-text-color: var(--mainmenu-nav-link-color);
--nav-bg-color: var(--color-link);
--border: 5px;
--muted-color: #6d757e;
--hr-color: var(--border-color, #dfe3e7);
--link-active-color: var(--link-color);
--code-color-ink: var(--code-color, #e93f8e);
--border-color-soft: var(--border-color, #dfe3e7);
--kbd-bg: var(--secondary-bg, #eaedf0);
--kbd-ink: var(--body-bg, #fff);
--toc-bg: var(--secondary-bg, #eaedf0);
--toc-ink: var(--color-primary, #112855);
--selection-bg: var(--highlight-bg, #fbeea8);
--selection-ink: var(--body-color, #22262a);
/* Palette */
--blue: #91a4ff;
--black: #000;
--indigo: #b19cff;
--purple: #c0a5ff;
--pink: #ff8fc0;
--red: #ff7a73;
--orange: #ff9c4d;
--yellow: #ffd166;
--green: #78d694;
--teal: #76e3ff;
--cyan: #6fb7ff;
--white: #fff;
/* Grays tuned for dark */
--gray-100: #161a20;
--gray-200: #1b2027;
--gray-300: #222831;
--gray-400: #2b323b;
--gray-500: #36404a;
--gray-600: #48525d;
--gray-700: #5b6672;
--gray-800: #cfd6de;
--gray-900: #e6ebf1;
/* Contextuals (keep brand hues) */
--primary: #010156;
--secondary: #48525d;
--success: #4aa664;
--info: #4f7aa0;
--warning: #c77a00;
--danger: #c23a31;
--light: #1b2027;
--dark: #0f1318;
/* RGB helpers */
--primary-rgb: 1,1,86;
--secondary-rgb: 72,82,93;
--success-rgb: 74,166,100;
--info-rgb: 79,122,160;
--warning-rgb: 199,122,0;
--danger-rgb: 194,58,49;
--light-rgb: 27,32,39;
--dark-rgb: 15,19,24;
/* Emphasis & subtle variants */
--primary-text-emphasis: #c7ccff;
--secondary-text-emphasis: #cfd6de;
--success-text-emphasis: #bde8c9;
--info-text-emphasis: #bcd6ee;
--warning-text-emphasis: #ffd9a6;
--danger-text-emphasis: #ffb7b2;
--light-text-emphasis: #d2d8df;
--dark-text-emphasis: #d2d8df;
--primary-bg-subtle: #0b1030;
--secondary-bg-subtle: #1e2430;
--success-bg-subtle: #0f2a1b;
--info-bg-subtle: #0d2232;
--warning-bg-subtle: #2a1e06;
--danger-bg-subtle: #2d1110;
--light-bg-subtle: #12161d;
--dark-bg-subtle: #1e2430;
--primary-border-subtle: #2b3a7a;
--secondary-border-subtle: #2b323b;
--success-border-subtle: #2b5b40;
--info-border-subtle: #254861;
--warning-border-subtle: #5a3c0e;
--danger-border-subtle: #5c2723;
--light-border-subtle: #222831;
--dark-border-subtle: #2b323b;
/* Typography & layout */
--body-font-family: var(--optain-cassiopeia-font-family-body, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
--body-font-size: 1rem;
--body-font-weight: 400;
--body-line-height: 1.5;
--body-color: #e6ebf1;
--body-color-rgb: 230, 235, 241;
--body-bg: #0e1318;
--body-bg-rgb: 14, 19, 24;
--emphasis-color: #fff;
--emphasis-color-rgb: 255, 255, 255;
--secondary-color: #e6ebf1bf;
--secondary-color-rgb: 230, 235, 241;
--secondary-bg: #151b22;
--secondary-bg-rgb: 21, 27, 34;
--tertiary-color: #e6ebf180;
--tertiary-color-rgb: 230, 235, 241;
--tertiary-bg: #10151b;
--tertiary-bg-rgb: 16, 21, 27;
--heading-color: #f1f5f9;
--link-color: #8ab4f8;
--link-color-rgb: 138, 180, 248;
--link-decoration: underline;
--link-hover-color: #c3d6ff;
--link-hover-color-rgb: 195, 214, 255;
--code-color: #ff7abd;
--highlight-color: #111;
--highlight-bg: #ffe28a1a;
--border-width: 1px;
--border-style: solid;
--border-color: #2b323b;
--border-color-translucent: #ffffff26;
--border-radius: .25rem;
--border-radius-sm: .2rem;
--border-radius-lg: .3rem;
--border-radius-xl: .3rem;
--border-radius-xxl: 2rem;
--border-radius-2xl: var(--border-radius-xxl);
--border-radius-pill: 50rem;
--box-shadow: 0 .5rem 1rem #00000066;
--box-shadow-sm: 0 .125rem .25rem #00000040;
--box-shadow-lg: 0 1rem 3rem #00000080;
--box-shadow-inset: inset 0 1px 2px #00000040;
--focus-ring-width: .25rem;
--focus-ring-opacity: .6;
--focus-ring-color: #5472ff66;
--form-valid-color: #78d694;
--form-valid-border-color: #78d694;
--form-invalid-color: #ff8e86;
--form-invalid-border-color: #ff8e86;
}
.btn {
--btn-padding-x: 1rem;
--btn-padding-y: 0.6rem;
--btn-font-family: ;
--btn-font-size: 1rem;
--btn-font-weight: 400;
--btn-line-height: 1.5;
--btn-color: var(--white);
--btn-bg: transparent;
--btn-border-width: 1px;
--btn-border-color: transparent;
--btn-border-radius: 0.25rem;
--btn-active-border-color: transparent;
--btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
--btn-disabled-opacity: 0.65;
--btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--btn-focus-shadow-rgb), .5);
display: inline-block;
padding: var(--btn-padding-y) var(--btn-padding-x);
font-family: var(--btn-font-family);
font-size: var(--btn-font-size);
font-weight: var(--btn-font-weight);
line-height: var(--btn-line-height);
color: var(--btn-color);
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border: var(--btn-border-width) solid var(--btn-border-color);
border-radius: var(--btn-border-radius);
background-color: var(--btn-bg);
-webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
-o-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
}
/* Buttons — inherit brand hues; ensure strong contrast on dark bg */
.btn-primary {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: hsl(240, 98%, 17%);
--btn-border-color: hsl(240, 98%, 17%);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #010149;
--btn-hover-border-color: #010145;
--btn-focus-shadow-rgb: 84, 114, 255;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #010145;
--btn-active-border-color: #010141;
}
.btn-secondary {
--btn-color: var(--nav-text-color);
--btn-bg: var(--nav-bg-color);
--btn-border-color: #3a4250;
--btn-hover-color: #fff;
--btn-hover-bg: #1b2a55;
--btn-hover-border-color: #162448;
--btn-focus-shadow-rgb: 84, 114, 255;
--btn-active-color: #fff;
--btn-active-bg: #162448;
--btn-active-border-color: #12203f;
}
/* Outline buttons on dark: keep readable borders */
.btn-outline-light {
--btn-color: #e6ebf1;
--btn-border-color: #e6ebf1;
--btn-hover-color: #111;
--btn-hover-bg: #e6ebf1;
--btn-hover-border-color: #e6ebf1;
--btn-active-color: #111;
--btn-active-bg: #d7dce2;
--btn-active-border-color: #d7dce2;
--gradient: none;
}
/* Links as buttons */
.btn-link {
--btn-font-weight: 400;
--btn-color: var(--link-color);
--btn-bg: transparent;
--btn-border-color: transparent;
--btn-hover-color: var(--link-hover-color);
--btn-hover-border-color: transparent;
--btn-active-color: var(--link-hover-color);
--btn-active-border-color: transparent;
--btn-disabled-color: #6d7781;
--btn-disabled-border-color: transparent;
--btn-box-shadow: none;
--btn-focus-shadow-rgb: 84, 114, 255;
text-decoration: underline;
}
.btn-secondary {
--btn-color: var(--nav-text-color);
--btn-bg: var(--nav-bg-color);
}

View File

@@ -0,0 +1,368 @@
@charset "UTF-8";
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/global/dark/colors_standard.css
* VERSION: 02.00
* BRIEF: Standard dark mode color definitions for Moko-Cassiopeia template
* =========================================================================
*/
/* -----------------------------------------------
* DARK THEME
* --------------------------------------------- */
:root[data-bs-theme='dark']{
/* System hint for native widgets */
color-scheme: dark;
/* Brand & links */
--color-primary: #112855;
--accent-color-primary: #3f8ff0;
--accent-color-secondary: #6fb3ff;
--mainmenu-nav-link-color: #fff;
--color-link: #224FAA;
--color-hover: #224FAA;
/* Header background (kept same image; works over dark bg) */
--header-background-image: url('../../../../../../media/templates/site/moko-cassiopeia/images/bg.svg'); --header-background-attachment: fixed;
--header-background-repeat: repeat;
--header-background-size: auto;
/* Section containers */
--container-below-topbar-bg-image: ;
--container-below-topbar-bg-color: ;
--container-below-topbar-bg-position: center;
--container-below-topbar-bg-attachment: fixed;
--container-below-topbar-bg-repeat: no-repeat;
--container-below-topbar-bg-size: cover;
--container-below-topbar-border: ;
--container-below-topbar-border-radius: ;
--container-top-a-bg-image: ;
--container-top-a-bg-color: ;
--container-top-a-bg-position: center;
--container-top-a-bg-attachment: fixed;
--container-top-a-bg-repeat: no-repeat;
--container-top-a-bg-size: cover;
--container-top-a-border: ;
--container-top-a-border-radius: ;
--container-top-b-bg-image: ;
--container-top-b-bg-color: ;
--container-top-b-bg-position: center;
--container-top-b-bg-attachment: fixed;
--container-top-b-bg-repeat: no-repeat;
--container-top-b-bg-size: cover;
--container-top-b-border: ;
--container-top-b-border-radius: ;
--container-toc-bg: ;
--container-toc-color: #dbe3ff;
--container-sidebar-bg-image: ;
--container-sidebar-bg-color: ;
--container-sidebar-bg-position: center;
--container-sidebar-bg-attachment: scroll;
--container-sidebar-bg-repeat: repeat;
--container-sidebar-bg-size: auto;
--container-sidebar-border: ;
--container-sidebar-border-radius: ;
--container-bottom-a-bg-image: ;
--container-bottom-a-bg-color: ;
--container-bottom-a-bg-position: center;
--container-bottom-a-bg-attachment: fixed;
--container-bottom-a-bg-repeat: no-repeat;
--container-bottom-a-bg-size: cover;
--container-bottom-a-border: ;
--container-bottom-a-border-radius: 5px;
--container-bottom-b-bg-image: ;
--container-bottom-b-bg-color: ;
--container-bottom-b-bg-position: center;
--container-bottom-b-bg-attachment: fixed;
--container-bottom-b-bg-repeat: no-repeat;
--container-bottom-b-bg-size: cover;
--container-bottom-b-border: ;
--container-bottom-b-border-radius: ;
/* Nav & accents */
--nav-text-color: var(--mainmenu-nav-link-color);
--nav-bg-color: var(--color-link);
--border: 5px;
--muted-color: #6d757e;
--hr-color: var(--border-color, #dfe3e7);
--link-active-color: var(--link-color);
--code-color-ink: var(--code-color, #e93f8e);
--border-color-soft: var(--border-color, #dfe3e7);
--kbd-bg: var(--secondary-bg, #eaedf0);
--kbd-ink: var(--body-bg, #fff);
--toc-bg: var(--secondary-bg, #eaedf0);
--toc-ink: var(--color-primary, #112855);
--selection-bg: var(--highlight-bg, #fbeea8);
--selection-ink: var(--body-color, #22262a);
/* Palette */
--blue: #91a4ff;
--black: #000;
--indigo: #b19cff;
--purple: #c0a5ff;
--pink: #ff8fc0;
--red: #ff7a73;
--orange: #ff9c4d;
--yellow: #ffd166;
--green: #78d694;
--teal: #76e3ff;
--cyan: #6fb7ff;
--white: #fff;
/* Grays tuned for dark */
--gray-100: #161a20;
--gray-200: #1b2027;
--gray-300: #222831;
--gray-400: #2b323b;
--gray-500: #36404a;
--gray-600: #48525d;
--gray-700: #5b6672;
--gray-800: #cfd6de;
--gray-900: #e6ebf1;
/* Contextuals (keep brand hues) */
--primary: #010156;
--secondary: #48525d;
--success: #4aa664;
--info: #4f7aa0;
--warning: #c77a00;
--danger: #c23a31;
--light: #1b2027;
--dark: #0f1318;
/* RGB helpers */
--primary-rgb: 1,1,86;
--secondary-rgb: 72,82,93;
--success-rgb: 74,166,100;
--info-rgb: 79,122,160;
--warning-rgb: 199,122,0;
--danger-rgb: 194,58,49;
--light-rgb: 27,32,39;
--dark-rgb: 15,19,24;
/* Emphasis & subtle variants */
--primary-text-emphasis: #c7ccff;
--secondary-text-emphasis: #cfd6de;
--success-text-emphasis: #bde8c9;
--info-text-emphasis: #bcd6ee;
--warning-text-emphasis: #ffd9a6;
--danger-text-emphasis: #ffb7b2;
--light-text-emphasis: #d2d8df;
--dark-text-emphasis: #d2d8df;
--primary-bg-subtle: #0b1030;
--secondary-bg-subtle: #1e2430;
--success-bg-subtle: #0f2a1b;
--info-bg-subtle: #0d2232;
--warning-bg-subtle: #2a1e06;
--danger-bg-subtle: #2d1110;
--light-bg-subtle: #12161d;
--dark-bg-subtle: #1e2430;
--primary-border-subtle: #2b3a7a;
--secondary-border-subtle: #2b323b;
--success-border-subtle: #2b5b40;
--info-border-subtle: #254861;
--warning-border-subtle: #5a3c0e;
--danger-border-subtle: #5c2723;
--light-border-subtle: #222831;
--dark-border-subtle: #2b323b;
/* Typography & layout */
--body-font-family: var(--optain-cassiopeia-font-family-body, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji');
--body-font-size: 1rem;
--body-font-weight: 400;
--body-line-height: 1.5;
--body-color: #e6ebf1;
--body-color-rgb: 230, 235, 241;
--body-bg: #0e1318;
--body-bg-rgb: 14, 19, 24;
--emphasis-color: #fff;
--emphasis-color-rgb: 255, 255, 255;
--secondary-color: #e6ebf1bf;
--secondary-color-rgb: 230, 235, 241;
--secondary-bg: #151b22;
--secondary-bg-rgb: 21, 27, 34;
--tertiary-color: #e6ebf180;
--tertiary-color-rgb: 230, 235, 241;
--tertiary-bg: #10151b;
--tertiary-bg-rgb: 16, 21, 27;
--heading-color: #f1f5f9;
--link-color: #8ab4f8;
--link-color-rgb: 138, 180, 248;
--link-decoration: underline;
--link-hover-color: #c3d6ff;
--link-hover-color-rgb: 195, 214, 255;
--code-color: #ff7abd;
--highlight-color: #111;
--highlight-bg: #ffe28a1a;
--border-width: 1px;
--border-style: solid;
--border-color: #2b323b;
--border-color-translucent: #ffffff26;
--border-radius: .25rem;
--border-radius-sm: .2rem;
--border-radius-lg: .3rem;
--border-radius-xl: .3rem;
--border-radius-xxl: 2rem;
--border-radius-2xl: var(--border-radius-xxl);
--border-radius-pill: 50rem;
--box-shadow: 0 .5rem 1rem #00000066;
--box-shadow-sm: 0 .125rem .25rem #00000040;
--box-shadow-lg: 0 1rem 3rem #00000080;
--box-shadow-inset: inset 0 1px 2px #00000040;
--focus-ring-width: .25rem;
--focus-ring-opacity: .6;
--focus-ring-color: #5472ff66;
--form-valid-color: #78d694;
--form-valid-border-color: #78d694;
--form-invalid-color: #ff8e86;
--form-invalid-border-color: #ff8e86;
}
.btn {
--btn-padding-x: 1rem;
--btn-padding-y: 0.6rem;
--btn-font-family: ;
--btn-font-size: 1rem;
--btn-font-weight: 400;
--btn-line-height: 1.5;
--btn-color: var(--white);
--btn-bg: transparent;
--btn-border-width: 1px;
--btn-border-color: transparent;
--btn-border-radius: 0.25rem;
--btn-active-border-color: transparent;
--btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
--btn-disabled-opacity: 0.65;
--btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--btn-focus-shadow-rgb), .5);
display: inline-block;
padding: var(--btn-padding-y) var(--btn-padding-x);
font-family: var(--btn-font-family);
font-size: var(--btn-font-size);
font-weight: var(--btn-font-weight);
line-height: var(--btn-line-height);
color: var(--btn-color);
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
border: var(--btn-border-width) solid var(--btn-border-color);
border-radius: var(--btn-border-radius);
background-color: var(--btn-bg);
-webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
-o-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;
}
/* Buttons — inherit brand hues; ensure strong contrast on dark bg */
.btn-primary {
--btn-color: hsl(0, 0%, 100%);
--btn-bg: hsl(240, 98%, 17%);
--btn-border-color: hsl(240, 98%, 17%);
--btn-hover-color: hsl(0, 0%, 100%);
--btn-hover-bg: #010149;
--btn-hover-border-color: #010145;
--btn-focus-shadow-rgb: 84, 114, 255;
--btn-active-color: hsl(0, 0%, 100%);
--btn-active-bg: #010145;
--btn-active-border-color: #010141;
}
.btn-secondary {
--btn-color: var(--nav-text-color);
--btn-bg: var(--nav-bg-color);
--btn-border-color: #3a4250;
--btn-hover-color: #fff;
--btn-hover-bg: #1b2a55;
--btn-hover-border-color: #162448;
--btn-focus-shadow-rgb: 84, 114, 255;
--btn-active-color: #fff;
--btn-active-bg: #162448;
--btn-active-border-color: #12203f;
}
/* Outline buttons on dark: keep readable borders */
.btn-outline-light {
--btn-color: #e6ebf1;
--btn-border-color: #e6ebf1;
--btn-hover-color: #111;
--btn-hover-bg: #e6ebf1;
--btn-hover-border-color: #e6ebf1;
--btn-active-color: #111;
--btn-active-bg: #d7dce2;
--btn-active-border-color: #d7dce2;
--gradient: none;
}
/* Links as buttons */
.btn-link {
--btn-font-weight: 400;
--btn-color: var(--link-color);
--btn-bg: transparent;
--btn-border-color: transparent;
--btn-hover-color: var(--link-hover-color);
--btn-hover-border-color: transparent;
--btn-active-color: var(--link-hover-color);
--btn-active-border-color: transparent;
--btn-disabled-color: #6d7781;
--btn-disabled-border-color: transparent;
--btn-box-shadow: none;
--btn-focus-shadow-rgb: 84, 114, 255;
text-decoration: underline;
}
.btn-secondary {
--btn-color: var(--nav-text-color);
--btn-bg: var(--nav-bg-color);
}

View File

@@ -1,126 +1,156 @@
@charset "UTF-8";
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/global/fonts-local_roboto.css
* VERSION: 02.00
* BRIEF: Local Roboto font-face definitions for the Moko-Cassiopeia template
* =========================================================================
*/
@font-face { @font-face {
font-family: "Roboto"; font-family: "Roboto";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Regular.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Regular.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Regular.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Regular.woff") format("woff");
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: "Roboto-Regular"; font-family: "Roboto-Regular";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Regular.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Regular.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Regular.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Regular.woff") format("woff");
} }
@font-face { @font-face {
font-family: "Roboto"; font-family: "Roboto";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-RegularItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-RegularItalic.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-RegularItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-RegularItalic.woff") format("woff");
font-weight: 400; font-weight: 400;
font-style: italic; font-style: italic;
} }
@font-face { @font-face {
font-family: "Roboto-RegularItalic"; font-family: "Roboto-RegularItalic";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-RegularItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-RegularItalic.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-RegularItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-RegularItalic.woff") format("woff");
} }
@font-face { @font-face {
font-family: "Roboto"; font-family: "Roboto";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Light.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Light.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Light.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Light.woff") format("woff");
font-weight: 300; font-weight: 300;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: "Roboto-Light"; font-family: "Roboto-Light";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Light.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Light.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Light.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Light.woff") format("woff");
} }
@font-face { @font-face {
font-family: "Roboto"; font-family: "Roboto";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-LightItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-LightItalic.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-LightItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-LightItalic.woff") format("woff");
font-weight: 300; font-weight: 300;
font-style: italic; font-style: italic;
} }
@font-face { @font-face {
font-family: "Roboto-LightItalic"; font-family: "Roboto-LightItalic";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-LightItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-LightItalic.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-LightItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-LightItalic.woff") format("woff");
} }
@font-face { @font-face {
font-family: "Roboto"; font-family: "Roboto";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Thin.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Thin.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Thin.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Thin.woff") format("woff");
font-weight: 100; font-weight: 100;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: "Roboto-Thin"; font-family: "Roboto-Thin";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Thin.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Thin.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Thin.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Thin.woff") format("woff");
} }
@font-face { @font-face {
font-family: "Roboto"; font-family: "Roboto";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-ThinItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-ThinItalic.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-ThinItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-ThinItalic.woff") format("woff");
font-weight: 100; font-weight: 100;
font-style: italic; font-style: italic;
} }
@font-face { @font-face {
font-family: "Roboto-ThinItalic"; font-family: "Roboto-ThinItalic";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-ThinItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-ThinItalic.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-ThinItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-ThinItalic.woff") format("woff");
} }
@font-face { @font-face {
font-family: "Roboto"; font-family: "Roboto";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Medium.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Medium.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Medium.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Medium.woff") format("woff");
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: "Roboto-Medium"; font-family: "Roboto-Medium";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Medium.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Medium.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Medium.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Medium.woff") format("woff");
} }
@font-face { @font-face {
font-family: "Roboto"; font-family: "Roboto";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-MediumItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-MediumItalic.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-MediumItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-MediumItalic.woff") format("woff");
font-weight: 500; font-weight: 500;
font-style: italic; font-style: italic;
} }
@font-face { @font-face {
font-family: "Roboto-MediumItalic"; font-family: "Roboto-MediumItalic";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-MediumItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-MediumItalic.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-MediumItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-MediumItalic.woff") format("woff");
} }
@font-face { @font-face {
font-family: "Roboto"; font-family: "Roboto";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Bold.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Bold.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Bold.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Bold.woff") format("woff");
font-weight: 700; font-weight: 700;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: "Roboto-Bold"; font-family: "Roboto-Bold";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Bold.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Bold.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Bold.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Bold.woff") format("woff");
} }
@font-face { @font-face {
font-family: "Roboto"; font-family: "Roboto";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BoldItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BoldItalic.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BoldItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BoldItalic.woff") format("woff");
font-weight: 700; font-weight: 700;
font-style: italic; font-style: italic;
} }
@font-face { @font-face {
font-family: "Roboto-BoldItalic"; font-family: "Roboto-BoldItalic";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BoldItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BoldItalic.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BoldItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BoldItalic.woff") format("woff");
} }
@font-face { @font-face {
font-family: "Roboto"; font-family: "Roboto";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Black.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Black.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Black.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Black.woff") format("woff");
font-weight: 900; font-weight: 900;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: "Roboto-Black"; font-family: "Roboto-Black";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Black.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Black.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Black.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-Black.woff") format("woff");
} }
@font-face { @font-face {
font-family: "Roboto"; font-family: "Roboto";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BlackItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BlackItalic.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BlackItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BlackItalic.woff") format("woff");
font-weight: 900; font-weight: 900;
font-style: italic; font-style: italic;
} }
@font-face { @font-face {
font-family: "Roboto-BlackItalic"; font-family: "Roboto-BlackItalic";
src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BlackItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BlackItalic.woff") format("woff"); src: url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BlackItalic.woff2") format("woff2"), url("../../../../../vendor/roboto-fontface/fonts/roboto/Roboto-BlackItalic.woff") format("woff");
} }
:root { :root {
--font-family-body: "Roboto", sans-serif; --font-family-body: "Roboto", sans-serif;
--font-family-headings: "Roboto", sans-serif; --font-family-headings: "Roboto", sans-serif;
--font-weight-headings: 700; --font-weight-headings: 700;
--font-weight-normal: 400; --font-weight-normal: 400;
} }

View File

@@ -1,14 +1,31 @@
/*! @charset "UTF-8";
* @package Joomla.Site /* =========================================================================
* @subpackage Templates.moko-cassiopeia * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @file /media/templates/sote/moko-cassiopeia/css/global/light/colors_alternative.css
* *
* @copyright 2025 Moko Consulting <https://mokoconsulting.tech> * This file is part of a Moko Consulting project.
* @license GNU General Public License version 2 or later; see LICENSE.txt
* *
* Website: https://mokoconsulting.tech * SPDX-License-Identifier: GPL-3.0-or-later
* Email: hello@mokoconsulting.tech *
* Phone: +1 (931) 279-6313 * 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/global/light/colors_alternative.css
* VERSION: 02.00
* BRIEF: Alternative light mode color definitions for Moko-Cassiopeia template
* =========================================================================
*/ */
/* ----------------------------------------------- /* -----------------------------------------------
@@ -26,7 +43,7 @@
--color-link: #224FAA; --color-link: #224FAA;
--color-hover: var(--accent-color-primary); --color-hover: var(--accent-color-primary);
--header-background-image: url('../../../../../../../media/templates/site/moko-cassiopeia/images/bg.svg'); --header-background-image: url('../../../../../../media/templates/site/moko-cassiopeia/images/bg.svg');
--header-background-attachment: fixed; --header-background-attachment: fixed;
--header-background-repeat: repeat; --header-background-repeat: repeat;
--header-background-size: auto; --header-background-size: auto;

View File

@@ -1,14 +1,31 @@
/*! @charset "UTF-8";
* @package Joomla.Site /* =========================================================================
* @subpackage Templates.moko-cassiopeia * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @file /media/templates/sote/moko-cassiopeia/css/global/light/colors_standard.css
* *
* @copyright 2025 Moko Consulting <https://mokoconsulting.tech> * This file is part of a Moko Consulting project.
* @license GNU General Public License version 2 or later; see LICENSE.txt
* *
* Website: https://mokoconsulting.tech * SPDX-License-Identifier: GPL-3.0-or-later
* Email: hello@mokoconsulting.tech *
* Phone: +1 (931) 279-6313 * 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/global/light/colors_standard.css
* VERSION: 02.00
* BRIEF: Standard light mode color definitions for Moko-Cassiopeia template
* =========================================================================
*/ */
/* ----------------------------------------------- /* -----------------------------------------------
@@ -26,7 +43,7 @@
--color-link: #224FAA; --color-link: #224FAA;
--color-hover: var(--accent-color-primary); --color-hover: var(--accent-color-primary);
--header-background-image: url('../../../../../../../media/templates/site/moko-cassiopeia/images/bg.svg'); --header-background-image: url('../../../../../../media/templates/site/moko-cassiopeia/images/bg.svg');
--header-background-attachment: fixed; --header-background-attachment: fixed;
--header-background-repeat: repeat; --header-background-repeat: repeat;
--header-background-size: auto; --header-background-size: auto;

View File

@@ -0,0 +1,118 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -1,16 +1,33 @@
/*! @charset "UTF-8";
* @package Joomla.Site /* =========================================================================
* @subpackage Templates.moko-cassiopeia * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @file /media/templates/sote/moko-cassiopeia/css/global/csocial-media-demos.css
* *
* @copyright 2025 Moko Consulting <https://mokoconsulting.tech> * This file is part of a Moko Consulting project.
* @license GNU General Public License version 2 or later; see LICENSE.txt
* *
* Website: https://mokoconsulting.tech * SPDX-License-Identifier: GPL-3.0-or-later
* Email: hello@mokoconsulting.tech *
* Phone: +1 (931) 279-6313 * 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/global/social-media-demo.css
* VERSION: 02.00
* BRIEF: Demo styles for showcasing social media elements in Moko-Cassiopeia template
* =========================================================================
*/ */
*
/* /*
====================================================================== ======================================================================
Social Media Demo — FULL CSS (Joomla-safe, fully scoped) Social Media Demo — FULL CSS (Joomla-safe, fully scoped)
@@ -19,10 +36,10 @@ Usage: Wrap your article markup in <div class="social-media-demo"> ... </div>
Version: 2.0 (2025-08-23) Version: 2.0 (2025-08-23)
How its organized: How its organized:
1) Container-level CSS variables (IMAGES ONLY). Colors are hard-coded per brand below. 1) Container-level CSS variables (IMAGES ONLY). Colors are hard-coded per brand below.
2) Base/layout styles (sections, header shell, placeholders, buttons). 2) Base/layout styles (sections, header shell, placeholders, buttons).
3) Platform brand colors (hard-coded) and cover height tweaks. 3) Platform brand colors (hard-coded) and cover height tweaks.
4) Image assignments (map classes like .fb-cover → variable --fb-cover-img). 4) Image assignments (map classes like .fb-cover → variable --fb-cover-img).
INSTRUCTIONS: INSTRUCTIONS:
- Save the images in their requried sizes into the [SITEROOT]/images/social/ folder with the exact names. - Save the images in their requried sizes into the [SITEROOT]/images/social/ folder with the exact names.
@@ -32,77 +49,77 @@ INSTRUCTIONS:
REQUIRED IMAGE SIZES — Social Media Demo Wireframes REQUIRED IMAGE SIZES — Social Media Demo Wireframes
Facebook Facebook
--fb-cover-img → Cover: 820×312 (desktop), 640×360 (mobile safe) --fb-cover-img → Cover: 820×312 (desktop), 640×360 (mobile safe)
--fb-avatar-img → Profile: 176×176 (shown as circle, but use square image) --fb-avatar-img → Profile: 176×176 (shown as circle, but use square image)
Twitter / X Twitter / X
--x-cover-img → Header: 1500×500 --x-cover-img → Header: 1500×500
--x-avatar-img → Profile: up to 400×400 (shown as circle, but use square image) --x-avatar-img → Profile: up to 400×400 (shown as circle, but use square image)
LinkedIn Company LinkedIn Company
--li-cover-img → Banner: ~1128×191 --li-cover-img → Banner: ~1128×191
--li-logo-img → Logo: up to 300×300 (rounded square) --li-logo-img → Logo: up to 300×300 (rounded square)
Google Business Profile Google Business Profile
--gmb-cover-img → Banner: ~960×200 (mobile ~960×140) --gmb-cover-img → Banner: ~960×200 (mobile ~960×140)
--gmb-logo-img → Logo: up to 300×300 (shown as circle, but use square image) --gmb-logo-img → Logo: up to 300×300 (shown as circle, but use square image)
Instagram Business Instagram Business
--ig-cover-img → Not always visible, safe 1080×608 for highlight background --ig-cover-img → Not always visible, safe 1080×608 for highlight background
--ig-avatar-img → Profile: 320×320 (shown as circle, but use square image) --ig-avatar-img → Profile: 320×320 (shown as circle, but use square image)
YouTube Channel YouTube Channel
--yt-cover-img → Channel art: 2560×1440 (safe area ~1546×423 center) --yt-cover-img → Channel art: 2560×1440 (safe area ~1546×423 center)
--yt-avatar-img → Channel icon: 800×800 (shown as circle, but use square image) --yt-avatar-img → Channel icon: 800×800 (shown as circle, but use square image)
TikTok Business TikTok Business
--tt-cover-img → Profile header: ~900×500 (safe area ~720×405) --tt-cover-img → Profile header: ~900×500 (safe area ~720×405)
--tt-avatar-img → Profile: 200×200 (shown as circle, but use square image) --tt-avatar-img → Profile: 200×200 (shown as circle, but use square image)
Pinterest Business Pinterest Business
--pin-cover-img → Board/brand banner: ~800×450 --pin-cover-img → Board/brand banner: ~800×450
--pin-avatar-img → Profile: 165×165 (shown as circle, but use square image) --pin-avatar-img → Profile: 165×165 (shown as circle, but use square image)
Snapchat Public Profile Snapchat Public Profile
--sc-cover-img → Banner: ~1080×1920 (stories/poster) --sc-cover-img → Banner: ~1080×1920 (stories/poster)
--sc-avatar-img → Bitmoji/Profile: 320×320 (shown as circle, but use square image) --sc-avatar-img → Bitmoji/Profile: 320×320 (shown as circle, but use square image)
Reddit Community Reddit Community
--rd-cover-img → Banner: 1920×384 --rd-cover-img → Banner: 1920×384
--rd-avatar-img → Community icon: 256×256 (shown as circle, but use square image) --rd-avatar-img → Community icon: 256×256 (shown as circle, but use square image)
====================================================================== */ ====================================================================== */
/* Container variables — IMAGES ONLY (safe-scoped) */ /* Container variables — IMAGES ONLY (safe-scoped) */
.social-media-demo { .social-media-demo {
--fb-cover-img: url('../../../../../image/social/fb-cover.jpg'); --fb-cover-img: url('../../../../../image/social/fb-cover.jpg');
--fb-avatar-img: url('../../../../../image/social/fb-avatar.jpg'); --fb-avatar-img: url('../../../../../image/social/fb-avatar.jpg');
--x-cover-img: url('../../../../../image/social/x-cover.jpg'); --x-cover-img: url('../../../../../image/social/x-cover.jpg');
--x-avatar-img: url('../../../../../image/social/x-avatar.jpg'); --x-avatar-img: url('../../../../../image/social/x-avatar.jpg');
--li-cover-img: url('../../../../../image/social/li-cover.jpg'); --li-cover-img: url('../../../../../image/social/li-cover.jpg');
--li-logo-img: url('../../../../../image/social/li-logo.jpg'); --li-logo-img: url('../../../../../image/social/li-logo.jpg');
--gmb-cover-img: url('../../../../../image/social/gmb-cover.jpg'); --gmb-cover-img: url('../../../../../image/social/gmb-cover.jpg');
--gmb-logo-img: url('../../../../../image/social/gmb-logo.jpg'); --gmb-logo-img: url('../../../../../image/social/gmb-logo.jpg');
--ig-cover-img: url('../../../../../image/social/ig-cover.jpg'); --ig-cover-img: url('../../../../../image/social/ig-cover.jpg');
--ig-avatar-img: url('../../../../../image/social/ig-avatar.jpg'); --ig-avatar-img: url('../../../../../image/social/ig-avatar.jpg');
--yt-cover-img: url('../../../../../image/social/yt-cover.jpg'); --yt-cover-img: url('../../../../../image/social/yt-cover.jpg');
--yt-avatar-img: url('../../../../../image/social/yt-avatar.jpg'); --yt-avatar-img: url('../../../../../image/social/yt-avatar.jpg');
--tt-cover-img: url('../../../../../image/social/tt-cover.jpg'); --tt-cover-img: url('../../../../../image/social/tt-cover.jpg');
--tt-avatar-img: url('../../../../../image/social/tt-avatar.jpg'); --tt-avatar-img: url('../../../../../image/social/tt-avatar.jpg');
--pin-cover-img: url('../../../../../image/social/pin-cover.jpg'); --pin-cover-img: url('../../../../../image/social/pin-cover.jpg');
--pin-avatar-img: url('../../../../../image/social/pin-avatar.jpg'); --pin-avatar-img: url('../../../../../image/social/pin-avatar.jpg');
--sc-cover-img: url('../../../../../image/social/sc-cover.jpg'); --sc-cover-img: url('../../../../../image/social/sc-cover.jpg');
--sc-avatar-img: url('../../../../../image/social/sc-avatar.jpg'); --sc-avatar-img: url('../../../../../image/social/sc-avatar.jpg');
--rd-cover-img: url('../../../../../image/social/rd-cover.jpg'); --rd-cover-img: url('../../../../../image/social/rd-cover.jpg');
--rd-avatar-img: url('../../../../../image/social/rd-avatar.jpg'); --rd-avatar-img: url('../../../../../image/social/rd-avatar.jpg');
} }
/* DO NOT TOUCH */ /* DO NOT TOUCH */

View File

@@ -1,49 +1,79 @@
@charset "UTF-8";
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/system/searchtools/searchtools.css
* VERSION: 02.00
* BRIEF: Stylesheet for Joomla search tools integration in Moko-Cassiopeia template
* =========================================================================
*/
.js-stools-container-bar { .js-stools-container-bar {
padding: 10px 20px; padding: 10px 20px;
} }
.js-stools-container-bar .btn-toolbar { .js-stools-container-bar .btn-toolbar {
-webkit-box-pack: end; -webkit-box-pack: end;
-ms-flex-pack: end; -ms-flex-pack: end;
justify-content: flex-end; justify-content: flex-end;
} }
.js-stools-container-bar .btn-toolbar > * { .js-stools-container-bar .btn-toolbar > * {
margin: 4px 0; margin: 4px 0;
-webkit-margin-end: 8px; -webkit-margin-end: 8px;
margin-inline-end: 8px; margin-inline-end: 8px;
} }
.js-stools-container-bar .btn-toolbar .js-stools-btn-clear { .js-stools-container-bar .btn-toolbar .js-stools-btn-clear {
background-color: hsl(207, 49%, 37%); background-color: hsl(207, 49%, 37%);
border: 0; border: 0;
} }
.js-stools-container-bar .ordering-select { .js-stools-container-bar .ordering-select {
display: -webkit-box; display: -webkit-box;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
} }
.js-stools-container-filters { .js-stools-container-filters {
display: none; display: none;
padding: 0 20px; padding: 0 20px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.js-stools-container-filters-visible { .js-stools-container-filters-visible {
display: grid; display: grid;
grid-gap: 8px; grid-gap: 8px;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
padding: 10px; padding: 10px;
background-color: hsl(0, 0%, 100%); background-color: hsl(0, 0%, 100%);
} }
.js-stools-container-filters > * { .js-stools-container-filters > * {
margin: 4px 0; margin: 4px 0;
-webkit-margin-end: 8px; -webkit-margin-end: 8px;
margin-inline-end: 8px; margin-inline-end: 8px;
} }
.js-stools-field-list + .js-stools-field-list { .js-stools-field-list + .js-stools-field-list {
-webkit-margin-start: 8px; -webkit-margin-start: 8px;
margin-inline-start: 8px; margin-inline-start: 8px;
} }
.js-stools-field-selector .form-select { .js-stools-field-selector .form-select {
width: auto; width: auto;
} }

View File

@@ -1,15 +1,31 @@
@charset "UTF-8"; @charset "UTF-8";
/*! /* =========================================================================
* @package Joomla.Site * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @subpackage Templates.moko-cassiopeia
* @file /media/templates/sote/moko-cassiopeia/css/template-rtl.css
* *
* @copyright 2025 Moko Consulting <https://mokoconsulting.tech> * This file is part of a Moko Consulting project.
* @license GNU General Public License version 2 or later; see LICENSE.txt
* *
* Website: https://mokoconsulting.tech * SPDX-License-Identifier: GPL-3.0-or-later
* Email: hello@mokoconsulting.tech *
* Phone: +1 (931) 279-6313 * 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/template-rtl.css
* VERSION: 02.00
* BRIEF: Right-to-left (RTL) layout stylesheet for Moko-Cassiopeia template
* =========================================================================
*/ */
/*! /*!
@@ -20,7 +36,6 @@
* sensitive rules are mirrored here to keep file size reasonable. * sensitive rules are mirrored here to keep file size reasonable.
*/ */
[dir="rtl"] .table-of-contents-ck-wrap { [dir="rtl"] .table-of-contents-ck-wrap {
width: 30%; width: 30%;

View File

@@ -1,15 +1,31 @@
@charset "UTF-8"; @charset "UTF-8";
/*! /* =========================================================================
* @package Joomla.Site * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @subpackage Templates.moko-cassiopeia
* @file /media/templates/sote/moko-cassiopeia/css/template.css
* *
* @copyright 2025 Moko Consulting <https://mokoconsulting.tech> * This file is part of a Moko Consulting project.
* @license GNU General Public License version 2 or later; see LICENSE.txt
* *
* Website: https://mokoconsulting.tech * SPDX-License-Identifier: GPL-3.0-or-later
* Email: hello@mokoconsulting.tech *
* Phone: +1 (931) 279-6313 * 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/template.css
* VERSION: 02.00
* BRIEF: Main stylesheet providing layout, typography, and component styles for Moko-Cassiopeia
* =========================================================================
*/ */
*, *,
@@ -17206,5 +17222,41 @@ body.site.error-page {
text-decoration: none; text-decoration: none;
} }
#mokoThemeFab .knob {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
border-radius: var(--border-radius-xxl);
background: var(--bs-body-bg, #fff);
box-shadow: var(--box-shadow);
transition: transform .2s ease;
}
#mokoThemeFab [role="switch"][aria-checked="true"] .knob {
transform: translateX(20px);
}
#mokoThemeFab [role="switch"][aria-checked="true"] .switch {
background: rgba(var(--secondary-color), .15);
}
button#mokoThemeSwitch {
border: unset;
background-color: unset;
}
#mokoThemeFab .label {
user-select: none;
font-size: .875rem;
}
#mokoThemeFab.debug-outline {
outline: 2px dashed var(--pink);
outline-offset: 2px;
}
/* SOCIAL MEDIA DEMOS */ /* SOCIAL MEDIA DEMOS */
@import url("global/social-media-demos.css"); @import url("global/social-media-demos.css");

View File

@@ -1,499 +1,528 @@
@charset "UTF-8"; @charset "UTF-8";
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/vendor/choicesjs/choices.css
* VERSION: 02.00
* BRIEF: Vendor stylesheet for Choices.js select and input enhancements in Moko-Cassiopeia
* =========================================================================
*/
/* =============================== /* ===============================
= Choices = = Choices =
=============================== */ =============================== */
.choices { .choices {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
margin-bottom: 24px; margin-bottom: 24px;
font-size: 16px; font-size: 16px;
} }
.choices:focus { .choices:focus {
outline: none; outline: none;
} }
.choices:last-child { .choices:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.choices.is-open { .choices.is-open {
overflow: initial; overflow: initial;
} }
.choices.is-disabled .choices__inner, .choices.is-disabled .choices__inner,
.choices.is-disabled .choices__input { .choices.is-disabled .choices__input {
background-color: #eaeaea; background-color: #eaeaea;
cursor: not-allowed; cursor: not-allowed;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
} }
.choices.is-disabled .choices__item { .choices.is-disabled .choices__item {
cursor: not-allowed; cursor: not-allowed;
} }
.choices [hidden] { .choices [hidden] {
display: none !important; display: none !important;
} }
.choices[data-type*=select-one] { .choices[data-type*=select-one] {
cursor: pointer; cursor: pointer;
} }
.choices[data-type*=select-one] .choices__inner { .choices[data-type*=select-one] .choices__inner {
padding-bottom: 7.5px; padding-bottom: 7.5px;
} }
.choices[data-type*=select-one] .choices__input { .choices[data-type*=select-one] .choices__input {
display: block; display: block;
width: 100%; width: 100%;
padding: 10px; padding: 10px;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
background-color: #fff; background-color: #fff;
margin: 0; margin: 0;
} }
.choices[data-type*=select-one] .choices__button { .choices[data-type*=select-one] .choices__button {
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg=="); background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==");
padding: 0; padding: 0;
background-size: 8px; background-size: 8px;
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 0; right: 0;
margin-top: -10px; margin-top: -10px;
margin-right: 25px; margin-right: 25px;
height: 20px; height: 20px;
width: 20px; width: 20px;
border-radius: 10em; border-radius: 10em;
opacity: 0.25; opacity: 0.25;
} }
.choices[data-type*=select-one] .choices__button:hover, .choices[data-type*=select-one] .choices__button:focus { .choices[data-type*=select-one] .choices__button:hover, .choices[data-type*=select-one] .choices__button:focus {
opacity: 1; opacity: 1;
} }
.choices[data-type*=select-one] .choices__button:focus { .choices[data-type*=select-one] .choices__button:focus {
-webkit-box-shadow: 0 0 0 2px #00bcd4; -webkit-box-shadow: 0 0 0 2px #00bcd4;
box-shadow: 0 0 0 2px #00bcd4; box-shadow: 0 0 0 2px #00bcd4;
} }
.choices[data-type*=select-one] .choices__item[data-value=""] .choices__button { .choices[data-type*=select-one] .choices__item[data-value=""] .choices__button {
display: none; display: none;
} }
.choices[data-type*=select-one]::after { .choices[data-type*=select-one]::after {
content: ""; content: "";
height: 0; height: 0;
width: 0; width: 0;
border-style: solid; border-style: solid;
border-color: #333 transparent transparent transparent; border-color: #333 transparent transparent transparent;
border-width: 5px; border-width: 5px;
position: absolute; position: absolute;
right: 11.5px; right: 11.5px;
top: 50%; top: 50%;
margin-top: -2.5px; margin-top: -2.5px;
pointer-events: none; pointer-events: none;
} }
.choices[data-type*=select-one].is-open::after { .choices[data-type*=select-one].is-open::after {
border-color: transparent transparent #333 transparent; border-color: transparent transparent #333 transparent;
margin-top: -7.5px; margin-top: -7.5px;
} }
.choices[data-type*=select-one][dir=rtl]::after { .choices[data-type*=select-one][dir=rtl]::after {
left: 11.5px; left: 11.5px;
right: auto; right: auto;
} }
.choices[data-type*=select-one][dir=rtl] .choices__button { .choices[data-type*=select-one][dir=rtl] .choices__button {
right: auto; right: auto;
left: 0; left: 0;
margin-left: 25px; margin-left: 25px;
margin-right: 0; margin-right: 0;
} }
.choices[data-type*=select-multiple] .choices__inner, .choices[data-type*=select-multiple] .choices__inner,
.choices[data-type*=text] .choices__inner { .choices[data-type*=text] .choices__inner {
cursor: text; cursor: text;
} }
.choices[data-type*=select-multiple] .choices__button, .choices[data-type*=select-multiple] .choices__button,
.choices[data-type*=text] .choices__button { .choices[data-type*=text] .choices__button {
position: relative; position: relative;
display: inline-block; display: inline-block;
margin-top: 0; margin-top: 0;
margin-right: -4px; margin-right: -4px;
margin-bottom: 0; margin-bottom: 0;
margin-left: 8px; margin-left: 8px;
padding-left: 16px; padding-left: 16px;
border-left: 1px solid #008fa1; border-left: 1px solid #008fa1;
background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg=="); background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSIjRkZGIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGQ9Ik0yLjU5Mi4wNDRsMTguMzY0IDE4LjM2NC0yLjU0OCAyLjU0OEwuMDQ0IDIuNTkyeiIvPjxwYXRoIGQ9Ik0wIDE4LjM2NEwxOC4zNjQgMGwyLjU0OCAyLjU0OEwyLjU0OCAyMC45MTJ6Ii8+PC9nPjwvc3ZnPg==");
background-size: 8px; background-size: 8px;
width: 8px; width: 8px;
line-height: 1; line-height: 1;
opacity: 0.75; opacity: 0.75;
border-radius: 0; border-radius: 0;
} }
.choices[data-type*=select-multiple] .choices__button:hover, .choices[data-type*=select-multiple] .choices__button:focus, .choices[data-type*=select-multiple] .choices__button:hover, .choices[data-type*=select-multiple] .choices__button:focus,
.choices[data-type*=text] .choices__button:hover, .choices[data-type*=text] .choices__button:hover,
.choices[data-type*=text] .choices__button:focus { .choices[data-type*=text] .choices__button:focus {
opacity: 1; opacity: 1;
} }
.choices__inner { .choices__inner {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
width: 100%; width: 100%;
background-color: #f9f9f9; background-color: #f9f9f9;
padding: 7.5px 7.5px 3.75px; padding: 7.5px 7.5px 3.75px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 2.5px; border-radius: 2.5px;
font-size: 14px; font-size: 14px;
min-height: 44px; min-height: 44px;
overflow: hidden; overflow: hidden;
} }
.is-focused .choices__inner, .is-open .choices__inner { .is-focused .choices__inner, .is-open .choices__inner {
border-color: #b7b7b7; border-color: #b7b7b7;
} }
.is-open .choices__inner { .is-open .choices__inner {
border-radius: 2.5px 2.5px 0 0; border-radius: 2.5px 2.5px 0 0;
} }
.is-flipped.is-open .choices__inner { .is-flipped.is-open .choices__inner {
border-radius: 0 0 2.5px 2.5px; border-radius: 0 0 2.5px 2.5px;
} }
.choices__list { .choices__list {
margin: 0; margin: 0;
padding-left: 0; padding-left: 0;
list-style: none; list-style: none;
} }
.choices__list--single { .choices__list--single {
display: inline-block; display: inline-block;
padding: 4px 16px 4px 4px; padding: 4px 16px 4px 4px;
width: 100%; width: 100%;
} }
[dir=rtl] .choices__list--single { [dir=rtl] .choices__list--single {
padding-right: 4px; padding-right: 4px;
padding-left: 16px; padding-left: 16px;
} }
.choices__list--single .choices__item { .choices__list--single .choices__item {
width: 100%; width: 100%;
} }
.choices__list--multiple { .choices__list--multiple {
display: inline; display: inline;
} }
.choices__list--multiple .choices__item { .choices__list--multiple .choices__item {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
border-radius: 20px; border-radius: 20px;
padding: 4px 10px; padding: 4px 10px;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
margin-right: 3.75px; margin-right: 3.75px;
margin-bottom: 3.75px; margin-bottom: 3.75px;
background-color: #00bcd4; background-color: #00bcd4;
border: 1px solid #00a5bb; border: 1px solid #00a5bb;
color: #fff; color: #fff;
word-break: break-all; word-break: break-all;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
} }
.choices__list--multiple .choices__item[data-deletable] { .choices__list--multiple .choices__item[data-deletable] {
padding-right: 5px; padding-right: 5px;
} }
[dir=rtl] .choices__list--multiple .choices__item { [dir=rtl] .choices__list--multiple .choices__item {
margin-right: 0; margin-right: 0;
margin-left: 3.75px; margin-left: 3.75px;
} }
.choices__list--multiple .choices__item.is-highlighted { .choices__list--multiple .choices__item.is-highlighted {
background-color: #00a5bb; background-color: #00a5bb;
border: 1px solid #008fa1; border: 1px solid #008fa1;
} }
.is-disabled .choices__list--multiple .choices__item { .is-disabled .choices__list--multiple .choices__item {
background-color: #aaaaaa; background-color: #aaaaaa;
border: 1px solid #919191; border: 1px solid #919191;
} }
.choices__list--dropdown { .choices__list--dropdown {
visibility: hidden; visibility: hidden;
z-index: 1; z-index: 1;
position: absolute; position: absolute;
width: 100%; width: 100%;
background-color: #fff; background-color: #fff;
border: 1px solid #ddd; border: 1px solid #ddd;
top: 100%; top: 100%;
margin-top: -1px; margin-top: -1px;
border-bottom-left-radius: 2.5px; border-bottom-left-radius: 2.5px;
border-bottom-right-radius: 2.5px; border-bottom-right-radius: 2.5px;
overflow: hidden; overflow: hidden;
word-break: break-all; word-break: break-all;
will-change: visibility; will-change: visibility;
} }
.choices__list--dropdown.is-active { .choices__list--dropdown.is-active {
visibility: visible; visibility: visible;
} }
.is-open .choices__list--dropdown { .is-open .choices__list--dropdown {
border-color: #b7b7b7; border-color: #b7b7b7;
} }
.is-flipped .choices__list--dropdown { .is-flipped .choices__list--dropdown {
top: auto; top: auto;
bottom: 100%; bottom: 100%;
margin-top: 0; margin-top: 0;
margin-bottom: -1px; margin-bottom: -1px;
border-radius: 0.25rem 0.25rem 0 0; border-radius: 0.25rem 0.25rem 0 0;
} }
.choices__list--dropdown .choices__list { .choices__list--dropdown .choices__list {
position: relative; position: relative;
max-height: 300px; max-height: 300px;
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
will-change: scroll-position; will-change: scroll-position;
} }
.choices__list--dropdown .choices__item { .choices__list--dropdown .choices__item {
position: relative; position: relative;
padding: 10px; padding: 10px;
font-size: 14px; font-size: 14px;
} }
[dir=rtl] .choices__list--dropdown .choices__item { [dir=rtl] .choices__list--dropdown .choices__item {
text-align: right; text-align: right;
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.choices__list--dropdown .choices__item--selectable { .choices__list--dropdown .choices__item--selectable {
padding-right: 100px; padding-right: 100px;
} }
.choices__list--dropdown .choices__item--selectable::after { .choices__list--dropdown .choices__item--selectable::after {
content: attr(data-select-text); content: attr(data-select-text);
font-size: 12px; font-size: 12px;
opacity: 0; opacity: 0;
position: absolute; position: absolute;
right: 10px; right: 10px;
top: 50%; top: 50%;
-webkit-transform: translateY(-50%); -webkit-transform: translateY(-50%);
transform: translateY(-50%); transform: translateY(-50%);
} }
[dir=rtl] .choices__list--dropdown .choices__item--selectable { [dir=rtl] .choices__list--dropdown .choices__item--selectable {
text-align: right; text-align: right;
padding-left: 100px; padding-left: 100px;
padding-right: 10px; padding-right: 10px;
} }
[dir=rtl] .choices__list--dropdown .choices__item--selectable::after { [dir=rtl] .choices__list--dropdown .choices__item--selectable::after {
right: auto; right: auto;
left: 10px; left: 10px;
} }
} }
.choices__list--dropdown .choices__item--selectable.is-highlighted { .choices__list--dropdown .choices__item--selectable.is-highlighted {
background-color: #f2f2f2; background-color: #f2f2f2;
} }
.choices__list--dropdown .choices__item--selectable.is-highlighted::after { .choices__list--dropdown .choices__item--selectable.is-highlighted::after {
opacity: 0.5; opacity: 0.5;
} }
.choices__item { .choices__item {
cursor: default; cursor: default;
} }
.choices__item--selectable { .choices__item--selectable {
cursor: pointer; cursor: pointer;
} }
.choices__item--disabled { .choices__item--disabled {
cursor: not-allowed; cursor: not-allowed;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
opacity: 0.5; opacity: 0.5;
} }
.choices__heading { .choices__heading {
font-weight: 600; font-weight: 600;
font-size: 12px; font-size: 12px;
padding: 10px; padding: 10px;
border-bottom: 1px solid #f7f7f7; border-bottom: 1px solid #f7f7f7;
color: gray; color: gray;
} }
.choices__button { .choices__button {
text-indent: -9999px; text-indent: -9999px;
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
appearance: none; appearance: none;
border: 0; border: 0;
background-color: transparent; background-color: transparent;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
cursor: pointer; cursor: pointer;
} }
.choices__button:focus { .choices__button:focus {
outline: none; outline: none;
} }
.choices__input { .choices__input {
display: inline-block; display: inline-block;
vertical-align: baseline; vertical-align: baseline;
background-color: #f9f9f9; background-color: #f9f9f9;
font-size: 14px; font-size: 14px;
margin-bottom: 5px; margin-bottom: 5px;
border: 0; border: 0;
border-radius: 0; border-radius: 0;
max-width: 100%; max-width: 100%;
padding: 4px 0 4px 2px; padding: 4px 0 4px 2px;
} }
.choices__input:focus { .choices__input:focus {
outline: 0; outline: 0;
} }
[dir=rtl] .choices__input { [dir=rtl] .choices__input {
padding-right: 2px; padding-right: 2px;
padding-left: 0; padding-left: 0;
} }
.choices__placeholder { .choices__placeholder {
opacity: 0.5; opacity: 0.5;
} }
/* ===== End of Choices ====== */ /* ===== End of Choices ====== */
.choices { .choices {
border: 1px solid hsl(210, 14%, 83%); border: 1px solid hsl(210, 14%, 83%);
border-radius: 0.25rem; border-radius: 0.25rem;
} }
.choices.is-focused { .choices.is-focused {
border-color: #8894aa; border-color: #8894aa;
-webkit-box-shadow: 0 0 0 0.25rem rgba(1, 1, 86, 0.25); -webkit-box-shadow: 0 0 0 0.25rem rgba(1, 1, 86, 0.25);
box-shadow: 0 0 0 0.25rem rgba(1, 1, 86, 0.25); box-shadow: 0 0 0 0.25rem rgba(1, 1, 86, 0.25);
} }
.choices__inner { .choices__inner {
padding: 0.4rem 1rem; padding: 0.4rem 1rem;
margin-bottom: 0; margin-bottom: 0;
font-size: 1rem; font-size: 1rem;
border: none; border: none;
border-radius: 0; border-radius: 0;
} }
.choices__input { .choices__input {
padding: 0; padding: 0;
margin-bottom: 0; margin-bottom: 0;
font-size: 1rem; font-size: 1rem;
background-color: transparent; background-color: transparent;
} }
.choices__input::-webkit-input-placeholder { .choices__input::-webkit-input-placeholder {
color: hsl(210, 9%, 31%); color: hsl(210, 9%, 31%);
opacity: 1; opacity: 1;
} }
.choices__input::-moz-placeholder { .choices__input::-moz-placeholder {
color: hsl(210, 9%, 31%); color: hsl(210, 9%, 31%);
opacity: 1; opacity: 1;
} }
.choices__input:-ms-input-placeholder { .choices__input:-ms-input-placeholder {
color: hsl(210, 9%, 31%); color: hsl(210, 9%, 31%);
opacity: 1; opacity: 1;
} }
.choices__input::-ms-input-placeholder { .choices__input::-ms-input-placeholder {
color: hsl(210, 9%, 31%); color: hsl(210, 9%, 31%);
opacity: 1; opacity: 1;
} }
.choices__input::placeholder { .choices__input::placeholder {
color: hsl(210, 9%, 31%); color: hsl(210, 9%, 31%);
opacity: 1; opacity: 1;
} }
.choices__list--dropdown { .choices__list--dropdown {
z-index: 1060; z-index: 1060;
} }
.choices__list--multiple .choices__item { .choices__list--multiple .choices__item {
position: relative; position: relative;
margin: 2px; margin: 2px;
background-color: var(--color-primary); background-color: var(--color-primary);
-webkit-margin-end: 2px; -webkit-margin-end: 2px;
margin-inline-end: 2px; margin-inline-end: 2px;
border: 0; border: 0;
border-radius: 0.25rem; border-radius: 0.25rem;
} }
.choices__list--multiple .choices__item.is-highlighted { .choices__list--multiple .choices__item.is-highlighted {
background-color: var(--color-primary); background-color: var(--color-primary);
opacity: 0.9; opacity: 0.9;
} }
.choices .choices__list--dropdown .choices__item { .choices .choices__list--dropdown .choices__item {
-webkit-padding-end: 10px; -webkit-padding-end: 10px;
padding-inline-end: 10px; padding-inline-end: 10px;
} }
.choices .choices__list--dropdown .choices__item--selectable::after { .choices .choices__list--dropdown .choices__item--selectable::after {
display: none; display: none;
} }
.choices__button_joomla { .choices__button_joomla {
position: relative; position: relative;
padding: 0 10px; padding: 0 10px;
color: inherit; color: inherit;
text-indent: -9999px; text-indent: -9999px;
cursor: pointer; cursor: pointer;
background: none; background: none;
border: 0; border: 0;
opacity: 0.5; opacity: 0.5;
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
appearance: none; appearance: none;
} }
.choices__button_joomla::before { .choices__button_joomla::before {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
display: block; display: block;
text-align: center; text-align: center;
text-indent: 0; text-indent: 0;
content: "×"; content: "×";
} }
.choices__button_joomla:hover, .choices__button_joomla:focus { .choices__button_joomla:hover, .choices__button_joomla:focus {
opacity: 1; opacity: 1;
} }
.choices__button_joomla:focus { .choices__button_joomla:focus {
outline: none; outline: none;
} }
.choices[data-type*=select-one] .choices__inner, .choices[data-type*=select-one] .choices__inner,
.choices[data-type*=select-multiple] .choices__inner { .choices[data-type*=select-multiple] .choices__inner {
-webkit-padding-end: 3rem; -webkit-padding-end: 3rem;
padding-inline-end: 3rem; padding-inline-end: 3rem;
cursor: pointer; cursor: pointer;
background: url("../../../images/select-bg.svg") no-repeat 100%/116rem; background: url("../../../images/select-bg.svg") no-repeat 100%/116rem;
background-color: hsl(210, 16%, 93%); background-color: hsl(210, 16%, 93%);
} }
[dir=rtl] .choices[data-type*=select-one] .choices__inner, [dir=rtl] .choices[data-type*=select-one] .choices__inner,
[dir=rtl] .choices[data-type*=select-multiple] .choices__inner { [dir=rtl] .choices[data-type*=select-multiple] .choices__inner {
background: url("../../../images/select-bg-rtl.svg") no-repeat 0/116rem; background: url("../../../images/select-bg-rtl.svg") no-repeat 0/116rem;
background-color: hsl(210, 16%, 93%); background-color: hsl(210, 16%, 93%);
} }
.choices[data-type*=select-one] .choices__item { .choices[data-type*=select-one] .choices__item {
display: -webkit-box; display: -webkit-box;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
-webkit-box-pack: justify; -webkit-box-pack: justify;
-ms-flex-pack: justify; -ms-flex-pack: justify;
justify-content: space-between; justify-content: space-between;
} }
.choices[data-type*=select-one] .choices__button_joomla { .choices[data-type*=select-one] .choices__button_joomla {
position: absolute; position: absolute;
top: 50%; top: 50%;
inset-inline-end: 0; inset-inline-end: 0;
width: 20px; width: 20px;
height: 20px; height: 20px;
padding: 0; padding: 0;
-webkit-margin-before: -10px; -webkit-margin-before: -10px;
margin-block-start: -10px; margin-block-start: -10px;
-webkit-margin-end: 50px; -webkit-margin-end: 50px;
margin-inline-end: 50px; margin-inline-end: 50px;
border-radius: 10em; border-radius: 10em;
opacity: 0.5; opacity: 0.5;
} }
.choices[data-type*=select-one] .choices__button_joomla:hover, .choices[data-type*=select-one] .choices__button_joomla:focus { .choices[data-type*=select-one] .choices__button_joomla:hover, .choices[data-type*=select-one] .choices__button_joomla:focus {
opacity: 1; opacity: 1;
} }
.choices[data-type*=select-one] .choices__button_joomla:focus { .choices[data-type*=select-one] .choices__button_joomla:focus {
-webkit-box-shadow: 0 0 0 2px #00bcd4; -webkit-box-shadow: 0 0 0 2px #00bcd4;
box-shadow: 0 0 0 2px #00bcd4; box-shadow: 0 0 0 2px #00bcd4;
} }
.choices[data-type*=select-one]::after { .choices[data-type*=select-one]::after {
display: none; display: none;
} }
.choices[data-type*=select-multiple] .choices__input, .choices[data-type*=select-multiple] .choices__input,
.choices[data-type*=text] .choices__input { .choices[data-type*=text] .choices__input {
padding: 0.2rem 0; padding: 0.2rem 0;
} }
.choices__heading { .choices__heading {
font-size: 1.2rem; font-size: 1.2rem;
} }

View File

@@ -1,146 +1,176 @@
@charset "UTF-8";
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/vendor/choicesjs/choices.css
* VERSION: 02.00
* BRIEF: Vendor stylesheet for Choices.js select and input enhancements in Moko-Cassiopeia
* =========================================================================
*/
@import "../../../../../../vendor/joomla-custom-elements/css/joomla-alert.css"; @import "../../../../../../vendor/joomla-custom-elements/css/joomla-alert.css";
#system-message-container:empty { #system-message-container:empty {
display: none; display: none;
margin-top: 0; margin-top: 0;
} }
#system-message-container joomla-alert { #system-message-container joomla-alert {
position: relative; position: relative;
display: -webkit-box; display: -webkit-box;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
width: 100%; width: 100%;
min-width: 16rem; min-width: 16rem;
padding: 0; padding: 0;
margin-bottom: 0; margin-bottom: 0;
color: var(--gray-dark); color: var(--gray-dark);
background-color: hsl(0, 0%, 100%); background-color: hsl(0, 0%, 100%);
border: 1px solid var(--alert-accent-color, transparent); border: 1px solid var(--alert-accent-color, transparent);
border-radius: 0.25rem; border-radius: 0.25rem;
-webkit-transition: opacity 0.15s linear; -webkit-transition: opacity 0.15s linear;
-o-transition: opacity 0.15s linear; -o-transition: opacity 0.15s linear;
transition: opacity 0.15s linear; transition: opacity 0.15s linear;
} }
#system-message-container joomla-alert + * { #system-message-container joomla-alert + * {
margin-top: 1rem; margin-top: 1rem;
} }
#system-message-container joomla-alert .alert-heading { #system-message-container joomla-alert .alert-heading {
display: -webkit-box; display: -webkit-box;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-box-direction: normal; -webkit-box-direction: normal;
-ms-flex-direction: column; -ms-flex-direction: column;
flex-direction: column; flex-direction: column;
-webkit-box-pack: center; -webkit-box-pack: center;
-ms-flex-pack: center; -ms-flex-pack: center;
justify-content: center; justify-content: center;
-ms-flex-line-pack: center; -ms-flex-line-pack: center;
align-content: center; align-content: center;
padding: 0.8rem; padding: 0.8rem;
color: var(--alert-heading-text); color: var(--alert-heading-text);
background: var(--alert-accent-color, transparent); background: var(--alert-accent-color, transparent);
} }
#system-message-container joomla-alert .alert-heading .message::before, #system-message-container joomla-alert .alert-heading .message::before,
#system-message-container joomla-alert .alert-heading .success::before { #system-message-container joomla-alert .alert-heading .success::before {
display: inline-block; display: inline-block;
width: 1em; width: 1em;
height: 1em; height: 1em;
content: ""; content: "";
background-image: url('data:image/svg+xml;utf8,<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path fill="rgba(255, 255, 255, .95)" d="M1299 813l-422 422q-19 19-45 19t-45-19l-294-294q-19-19-19-45t19-45l102-102q19-19 45-19t45 19l147 147 275-275q19-19 45-19t45 19l102 102q19 19 19 45t-19 45zm141 83q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/></svg>'); background-image: url('data:image/svg+xml;utf8,<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path fill="rgba(255, 255, 255, .95)" d="M1299 813l-422 422q-19 19-45 19t-45-19l-294-294q-19-19-19-45t19-45l102-102q19-19 45-19t45 19l147 147 275-275q19-19 45-19t45 19l102 102q19 19 19 45t-19 45zm141 83q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/></svg>');
background-size: 100%; background-size: 100%;
} }
#system-message-container joomla-alert .alert-heading .notice::before, #system-message-container joomla-alert .alert-heading .notice::before,
#system-message-container joomla-alert .alert-heading .info::before { #system-message-container joomla-alert .alert-heading .info::before {
display: inline-block; display: inline-block;
width: 1em; width: 1em;
height: 1em; height: 1em;
content: ""; content: "";
background-image: url('data:image/svg+xml;utf8,<svg width="1792" height="1792" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="rgba(255, 255, 255, .95)" d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"/></svg>'); background-image: url('data:image/svg+xml;utf8,<svg width="1792" height="1792" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="rgba(255, 255, 255, .95)" d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"/></svg>');
background-size: 100%; background-size: 100%;
} }
#system-message-container joomla-alert .alert-heading .warning::before { #system-message-container joomla-alert .alert-heading .warning::before {
display: inline-block; display: inline-block;
width: 1em; width: 1em;
height: 1em; height: 1em;
content: ""; content: "";
background-image: url('data:image/svg+xml;utf8,<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path fill="rgba(255, 255, 255, .95)" d="M1024 1375v-190q0-14-9.5-23.5t-22.5-9.5h-192q-13 0-22.5 9.5t-9.5 23.5v190q0 14 9.5 23.5t22.5 9.5h192q13 0 22.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11h-220q-11 0-24 11-10 7-10 21l17 457q0 10 10 16.5t24 6.5h185q14 0 23.5-6.5t10.5-16.5zm-14-934l768 1408q35 63-2 126-17 29-46.5 46t-63.5 17h-1536q-34 0-63.5-17t-46.5-46q-37-63-2-126l768-1408q17-31 47-49t65-18 65 18 47 49z"/></svg>'); background-image: url('data:image/svg+xml;utf8,<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path fill="rgba(255, 255, 255, .95)" d="M1024 1375v-190q0-14-9.5-23.5t-22.5-9.5h-192q-13 0-22.5 9.5t-9.5 23.5v190q0 14 9.5 23.5t22.5 9.5h192q13 0 22.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11h-220q-11 0-24 11-10 7-10 21l17 457q0 10 10 16.5t24 6.5h185q14 0 23.5-6.5t10.5-16.5zm-14-934l768 1408q35 63-2 126-17 29-46.5 46t-63.5 17h-1536q-34 0-63.5-17t-46.5-46q-37-63-2-126l768-1408q17-31 47-49t65-18 65 18 47 49z"/></svg>');
background-size: 100%; background-size: 100%;
} }
#system-message-container joomla-alert .alert-heading .error::before, #system-message-container joomla-alert .alert-heading .error::before,
#system-message-container joomla-alert .alert-heading .danger::before { #system-message-container joomla-alert .alert-heading .danger::before {
display: inline-block; display: inline-block;
width: 1em; width: 1em;
height: 1em; height: 1em;
content: ""; content: "";
background-image: url('data:image/svg+xml;utf8,<svg width="1792" height="1792" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="rgba(255, 255, 255, .95)" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z"/></svg>'); background-image: url('data:image/svg+xml;utf8,<svg width="1792" height="1792" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="rgba(255, 255, 255, .95)" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm101.8-262.2L295.6 256l62.2 62.2c4.7 4.7 4.7 12.3 0 17l-22.6 22.6c-4.7 4.7-12.3 4.7-17 0L256 295.6l-62.2 62.2c-4.7 4.7-12.3 4.7-17 0l-22.6-22.6c-4.7-4.7-4.7-12.3 0-17l62.2-62.2-62.2-62.2c-4.7-4.7-4.7-12.3 0-17l22.6-22.6c4.7-4.7 12.3-4.7 17 0l62.2 62.2 62.2-62.2c4.7-4.7 12.3-4.7 17 0l22.6 22.6c4.7 4.7 4.7 12.3 0 17z"/></svg>');
background-size: 100%; background-size: 100%;
} }
#system-message-container joomla-alert .alert-wrapper { #system-message-container joomla-alert .alert-wrapper {
width: 100%; width: 100%;
} }
#system-message-container joomla-alert .alert-link { #system-message-container joomla-alert .alert-link {
color: var(--success, inherit); color: var(--success, inherit);
} }
#system-message-container joomla-alert[type=success], #system-message-container joomla-alert[type=message] { #system-message-container joomla-alert[type=success], #system-message-container joomla-alert[type=message] {
--alert-accent-color: var(--success); --alert-accent-color: var(--success);
--alert-heading-text: hsla(0, 0%, 100%, .95); --alert-heading-text: hsla(0, 0%, 100%, .95);
--alert-close-button: var(--success); --alert-close-button: var(--success);
background-color: hsl(0, 0%, 100%); background-color: hsl(0, 0%, 100%);
} }
#system-message-container joomla-alert[type=info], #system-message-container joomla-alert[type=notice] { #system-message-container joomla-alert[type=info], #system-message-container joomla-alert[type=notice] {
--alert-accent-color: var(--info); --alert-accent-color: var(--info);
--alert-heading-text: hsla(0, 0%, 100%, .95); --alert-heading-text: hsla(0, 0%, 100%, .95);
--alert-close-button: var(--info); --alert-close-button: var(--info);
background-color: hsl(0, 0%, 100%); background-color: hsl(0, 0%, 100%);
} }
#system-message-container joomla-alert[type=warning] { #system-message-container joomla-alert[type=warning] {
--alert-accent-color: var(--warning); --alert-accent-color: var(--warning);
--alert-heading-text: hsla(0, 0%, 100%, .95); --alert-heading-text: hsla(0, 0%, 100%, .95);
--alert-close-button: var(--warning); --alert-close-button: var(--warning);
background-color: hsl(0, 0%, 100%); background-color: hsl(0, 0%, 100%);
} }
#system-message-container joomla-alert[type=error], #system-message-container joomla-alert[type=danger] { #system-message-container joomla-alert[type=error], #system-message-container joomla-alert[type=danger] {
--alert-accent-color: var(--danger); --alert-accent-color: var(--danger);
--alert-heading-text: hsla(0, 0%, 100%, .95); --alert-heading-text: hsla(0, 0%, 100%, .95);
--alert-close-button: var(--danger); --alert-close-button: var(--danger);
background-color: hsl(0, 0%, 100%); background-color: hsl(0, 0%, 100%);
} }
#system-message-container joomla-alert .joomla-alert--close, #system-message-container joomla-alert .joomla-alert--close,
#system-message-container joomla-alert .joomla-alert-button--close { #system-message-container joomla-alert .joomla-alert-button--close {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
padding: 0.2rem 0.8rem; padding: 0.2rem 0.8rem;
font-size: 2rem; font-size: 2rem;
color: var(--alert-close-button); color: var(--alert-close-button);
background: none; background: none;
border: 0; border: 0;
opacity: 1; opacity: 1;
} }
#system-message-container joomla-alert .joomla-alert--close:hover, #system-message-container joomla-alert .joomla-alert--close:focus, #system-message-container joomla-alert .joomla-alert--close:hover, #system-message-container joomla-alert .joomla-alert--close:focus,
#system-message-container joomla-alert .joomla-alert-button--close:hover, #system-message-container joomla-alert .joomla-alert-button--close:hover,
#system-message-container joomla-alert .joomla-alert-button--close:focus { #system-message-container joomla-alert .joomla-alert-button--close:focus {
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
opacity: 0.75; opacity: 0.75;
} }
[dir=rtl] #system-message-container joomla-alert .joomla-alert--close, [dir=rtl] #system-message-container joomla-alert .joomla-alert--close,
[dir=rtl] #system-message-container joomla-alert .joomla-alert-button--close { [dir=rtl] #system-message-container joomla-alert .joomla-alert-button--close {
right: auto; right: auto;
left: 0; left: 0;
padding: 0.2rem 0.6rem; padding: 0.2rem 0.6rem;
} }
#system-message-container joomla-alert div { #system-message-container joomla-alert div {
font-size: 1rem; font-size: 1rem;
} }
#system-message-container joomla-alert div .alert-message { #system-message-container joomla-alert div .alert-message {
padding: 0.3rem 2rem 0.3rem 0.3rem; padding: 0.3rem 2rem 0.3rem 0.3rem;
margin: 0.5rem; margin: 0.5rem;
} }
[dir=rtl] #system-message-container joomla-alert div .alert-message { [dir=rtl] #system-message-container joomla-alert div .alert-message {
padding: 0.3rem 0.3rem 0.3rem 2rem; padding: 0.3rem 0.3rem 0.3rem 2rem;
} }
#system-message-container joomla-alert div .alert-message:not(:first-of-type) { #system-message-container joomla-alert div .alert-message:not(:first-of-type) {
border-top: 1px solid var(--alert-accent-color); border-top: 1px solid var(--alert-accent-color);
} }

View File

@@ -1,3 +1,33 @@
@charset "UTF-8";
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/css/vendor/vmbasic.css
* VERSION: 02.00
* BRIEF: Vendor stylesheet providing base styles for VM Basic in Moko-Cassiopeia
* =========================================================================
*/
/* Bootstrap */ /* Bootstrap */
.dropdown-menu { .dropdown-menu {
border-radius: 0; border-radius: 0;

View File

@@ -0,0 +1,617 @@
/* Bootstrap */
.dropdown-menu {
border-radius: 0;
--bs-dropdown-zindex: 1030;
}
.dropdown-toggle {
display: flex;
align-items: center;
}
.dropdown-toggle::after {
content: "";
border: none;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" fill="currentColor" class="bi bi-chevron-down" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708"/></svg>');
background-repeat: no-repeat;
width: 10px;
height: 10px
}
.btn {
border-radius: 0
}
.input-group .btn {
font-size: 14px;
border-radius: var(--bs-border-radius);
}
.btn-primary {
--bs-btn-bg: #333;
--bs-btn-border-color: #333;
--bs-btn-hover-bg: #555;
--bs-btn-hover-border-color: #555;
--bs-btn-focus-shadow-rgb: 49, 132, 253;
--bs-btn-active-bg: #555;
--bs-btn-active-border-color: #555;
--bs-btn-disabled-bg: #A0A0A0;
--bs-btn-disabled-border-color: #A0A0A0;
}
.btn-secondary {
--bs-btn-color: #333;
--bs-btn-bg: #EFEFEF;
--bs-btn-border-color: #EFEFEF;
--bs-btn-hover-bg: #333;
--bs-btn-hover-border-color: #333;
--bs-btn-active-bg: #333;
--bs-btn-active-border-color: #333;
}
.btn-check:checked + .btn, .btn.active, .btn.show, .btn:first-child:active, :not(.btn-check) + .btn:active {
color: #A0A0A0;
}
.text-secondary {
color: #A0A0A0 !important;
}
.form-control, .form-select {
font-size: 14px
}
form .form-control, form .form-select {
border-color: #C7C7C7
}
/* General styles */
body {
font-size: 15px;
color: #333;
}
a, .btn-link {
color: #A0A0A0;
text-decoration: none
}
a:hover, .btn-link:hover {
color: #333;
}
img {
max-width: 100%;
height: auto;
aspect-ratio: attr(width) / attr(height);
}
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
margin-bottom: 1em;
color: #333;
}
h1 {
font-size: 32px
}
h2 {
font-size: 28px
}
h3 {
font-size: 25px
}
h4 {
font-size: 22px
}
h5 {
font-size: 20px
}
h6 {
font-size: 18px
}
.toolbar {
font-size: 14px;
padding: 9px 0;
background-color: #EFEFEF
}
.toolbar a, .toolbar .btn-link {
color: inherit;
}
.toolbar .btn svg {
margin-right: 5px;
}
.toolbar svg {
line-height: 16px;
vertical-align: sub;
}
.top-bar {
font-weight: bold;
}
header p {
margin: 0
}
.toolbar .dropdown-menu {
font-size: 14px;
line-height: 14px;
min-width: 100%;
width: max-content;
}
.form-control-feedback {
display: block;
font-size: 14px;
color: red;
}
/* Main menu */
.main-menu {
background-color: #555;
color: #fff;
}
.main-menu .nav-item {
position: relative;
}
.main-menu a, .main-menu span {
position: relative;
display: block;
padding: 14px 18px;
color: #fff;
background-color: #555;
transition: background-color linear 250ms
}
.main-menu .active > a, .main-menu .active > span, .main-menu a:hover, .main-menu span:hover {
background-color: #000;
transition: background-color linear 250ms
}
.main-menu .parent > a::after, .main-menu .parent > span::after {
content: "";
display: inline-block;
width: 10px;
height: 10px;
background-color: #fff;
margin-left: 5px;
-webkit-mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" fill="currentColor" class="bi bi-chevron-down" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708"/></svg>');
mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" fill="currentColor" class="bi bi-chevron-down" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708"/></svg>');
}
.main-menu .mod-menu__sub .parent > a::after, .main-menu .mod-menu__sub .parent > span::after {
position: absolute;
right: 9px;
top: 50%;
transform: translateY(-50%) rotate(-90deg);
}
.main-menu .mod-menu__sub {
position: absolute;
left: 0;
top: 100%;
z-index: 1022;
min-width: 200px;
opacity: 0;
visibility: hidden;
transition: all linear 250ms
}
.main-menu .parent:hover > .mod-menu__sub {
opacity: 1;
visibility: visible;
box-shadow: 0 5px 15px 0 rgba(0,0,0,0.3);
transition: all linear 250ms
}
.mod-menu__sub .mod-menu__sub {
left: 100%;
top: 0
}
/* Banners */
.banner-section p {
margin: 0
}
/* Modules */
.mod-breadcrumbs {
background-color: #EFEFEF;
font-size: 14px;
white-space: nowrap;
overflow: auto;
flex-wrap: nowrap;
}
.breadcrumb-item + .breadcrumb-item::before {
float: none;
}
.mod-breadcrumbs a, .manufacturer-details-view a:not(.btn) {
color: #71ABD6;
}
.breadcrumb-item.active {
color: #A0A0A0;
}
aside .module-title {
font-size: 24px;
font-weight: normal;
border-bottom: 1px solid #dee2e6;
padding-bottom: 15px;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: space-between;
}
/* Footer */
footer {
padding: 50px 0;
background-color: #EFEFEF;
}
footer .module-title {
font-size: 18px;
margin-bottom: 20px;
}
footer .nav {
flex-direction: column;
}
footer ul {
list-style: none;
padding: 0;
margin: 0
}
footer .nav-item, footer li {
margin-bottom: 10px;
}
footer a {
color: #333;
}
footer a:hover {
color: #A0A0A0;
}
/* Forms */
textarea {
min-height: 100px
}
.control-label {
margin-bottom: 5px;
}
/*********
Virtuemart
*********/
/* VM Search module */
.vmbasic-search input {
border-color: #A0A0A0;
border-radius: 0;
height: 40px;
}
.mod-vm-search .btn-svg {
padding: 0 10px;
position: absolute;
z-index: 5;
top: 0;
right: 0;
bottom: 0;
height: 40px;
}
.vm-search-custom-search-input input, #vm-orderby-select {
border-radius: var(--bs-border-radius) !important;
}
/* VM cart module */
.vmCartModule .btn-link {
color: #333;
}
/* Product page */
.manufacturer a {
color: #71ABD6;
}
/* Account */
.vm-add-edit-address > a, .vm-order-list a {
font-weight: 600;
color: #71ABD6
}
/* Checkout */
.vm-coupon-container .btn {
border-radius: var(--bs-border-radius);
}
#checkoutForm .details {
font-weight: 600;
color: #71ABD6
}
/*********
Joomla
*********/
/* com_content */
.item-content .page-header h2 {
font-size: 20px;
}
.article-info {
display: flex;
flex-wrap: wrap;
font-size: 14px;
margin-bottom: 5px;
}
.item-image {
display: block;
margin: 0 auto 30px;
}
.article-info > * {
margin-right: 8px
}
.com-content-article__links {
list-style: none;
padding: 0;
margin: 30px 0;
}
.com-content-article__links a, .items-more a {
color: #71ABD6
}
.active > .page-link, .page-link.active {
background-color: #555555;
border-color: #555555;
}
.page-link, .page-link:hover {
color: #333;
}
.pagenavigation {
margin: 30px 0;
}
.pagenavigation .next {
margin-left: auto;
}
/* tags */
.com-tags-tag-list__category, .com-tags-tag__category {
margin-top: 30px;
}
.tag-category .list-group-item h3 {
margin: 0;
font-size: 16px;
}
.tags .btn {
font-size: 12px;
padding: 0 5px;
color: #fff;
}
/* User */
.com-users-login.login, .com-users-reset, .com-users-remind, .com-users-registration {
max-width: 400px;
margin: auto;
padding: 15px;
border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;
}
.control-group, .com-users-login__remember {
margin-bottom: 15px;
}
.com-users-reset legend, .com-users-remind legend {
font-size: 14px
}
/* Contact */
#contact-form legend {
font-size: 16px;
font-weight: 600;
}
.com-contact-featured__table {
margin-top: 30px;
}
/* Modules */
.sidebar-right ul, .sidebar-left ul {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar-right li, .sidebar-left li {
margin-bottom: 10px
}
.mod-login__userdata > div {
margin-bottom: 15px;
}
.awesomplete input {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.mod-login__userdata.userdata {
padding: 15px;
border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;
}
#Passkey {
width: 24px;
}
/* offcanvas*/
.offcanvas-body .mod-menu {
flex-direction: column;
}
.offcanvas-body .mod-menu__sub {
display: none;
padding: 10px
}
.offcanvas-body .nav-item {
padding: 5px 0;
border-bottom: 1px solid var(--bs-border-color);
position: relative;
}
.offcanvas-body .nav-item:last-child {
border-bottom: none;
}
.offcanvas-body a {
font-size: 15px;
color: #333
}
.offcanvas-body .active > a {
font-weight: bold;
}
.offcanvas-body .subtoggle {
padding: 0;
background-color: transparent;
border: none;
width: 30px;
height: 30px;
position: absolute;
right: 0;
top: 1px;
z-index: 1
}
.offcanvas-body .subtoggle.open {
transform: rotate(-180deg);
transition: all linear 200ms
}
/* To top */
.back-to-top-link {
display: none;
position: fixed;
right: 12px;
bottom: 12px;
z-index: 1020;
}
@media screen and (max-width: 1080px) {
body {
font-size: 14px
}
}
@media screen and (max-width: 991px) {
h1 {
font-size: 24px
}
h2 {
font-size: 22px
}
h3 {
font-size: 20px
}
h4 {
font-size: 18px
}
h5 {
font-size: 16px
}
h6 {
font-size: 15px
}
.main-search {
display: none;
}
}
@media screen and (max-width: 490px) {
.toolbar {
padding: 6px 0;
}
.toolbar .bg-alt {
background-color: #333;
color: #fff;
}
.top-bar {
background-color: #333;
color: #fff;
}
.top-bar a {
color: #fff;
}
.cart-module .bi-cart3 {
margin-top: 2px
}
.cart-module .total_products {
display: none;
}
.vmCartModule .dropdown-menu {
margin-right: -10px !important;
}
.cart-module .dropdown-toggle::after {
content: none;
}
.com-contact-featured__items, .com-contact-category__items {
overflow-x: auto;
}
footer {
padding-bottom: 30px;
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1854.54 295" preserveAspectRatio="xMinYMid" width="1854.54" height="295"><path d="m14.47 145.7 6.898 6.899c0.102 0.101 0.199 0.101 0.3 0.101 0.102 0 0.2 0 0.302-0.101l6.898-6.899c0.102-0.1 0.102-0.2 0.102-0.3s0-0.2-0.102-0.3l-0.7-0.7c-0.101-0.1-0.198-0.1-0.3-0.1-0.101 0-0.2 0-0.301 0.1l-5.8 5.8-5.801-5.8c-0.101-0.1-0.199-0.1-0.301-0.1-0.101 0-0.199 0-0.301 0.1l-0.699 0.7c-0.101 0.1-0.101 0.2-0.101 0.3-0.294 0.1-0.194 0.2-0.094 0.3z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1854.54 295" preserveAspectRatio="xMinYMid" width="1854.54" height="295"><path d="M1825.1,145.7l6.9,6.9c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l6.9-6.9c0.1-0.1,0.1-0.2,0.1-0.3c0-0.1,0-0.2-0.1-0.3l-0.7-0.7c-0.1-0.1-0.2-0.1-0.3-0.1s-0.2,0-0.3,0.1l-5.8,5.8l-5.8-5.8c-0.1-0.1-0.2-0.1-0.3-0.1c-0.1,0-0.2,0-0.3,0.1l-0.7,0.7c-0.1,0.1-0.1,0.2-0.1,0.3C1824.9,145.5,1825,145.6,1825.1,145.7z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 459 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1854.539" height="295" preserveAspectRatio="xMinYMin meet"><path d="M13.573 145.7l6.9 6.9c.1.1.2.1.3.1s.2 0 .3-.1l6.9-6.9c.1-.1.1-.2.1-.3s0-.2-.1-.3l-.7-.7c-.1-.1-.2-.1-.3-.1s-.2 0-.3.1l-5.8 5.8-5.8-5.8c-.1-.1-.2-.1-.3-.1s-.2 0-.3.1l-.7.7c-.1.1-.1.2-.1.3-.296.1-.195.2-.096.3h-.002z"/><path fill="#fff" d="M44.54 0h1810v295h-1810z"/></svg>

After

Width:  |  Height:  |  Size: 387 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1854.54 295" preserveAspectRatio="xMinYMid" width="1854.54" height="295"><path d="M1825.1,145.7l6.9,6.9c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l6.9-6.9c0.1-0.1,0.1-0.2,0.1-0.3c0-0.1,0-0.2-0.1-0.3l-0.7-0.7c-0.1-0.1-0.2-0.1-0.3-0.1s-0.2,0-0.3,0.1l-5.8,5.8l-5.8-5.8c-0.1-0.1-0.2-0.1-0.3-0.1c-0.1,0-0.2,0-0.3,0.1l-0.7,0.7c-0.1,0.1-0.1,0.2-0.1,0.3C1824.9,145.5,1825,145.6,1825.1,145.7z" fill="#000"/><rect width="1810" height="295" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,147 +1,170 @@
/** /* =========================================================================
* darkmode-toggle.js — Floating theme switch (class-based, CSP-proof) * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @version 2.2.1 *
* Storage key: "theme" -> "light" | "dark" * This file is part of a Moko Consulting project.
* Corner from <body data-theme-fab-pos="br|bl|tr|tl"> (default br) *
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/js/darkmode-toggle.js
* VERSION: 02.00
* BRIEF: JavaScript logic for dark mode toggle functionality in Moko-Cassiopeia
* =========================================================================
*/ */
(function () { (function () {
'use strict'; 'use strict';
var STORAGE_KEY = 'theme'; var STORAGE_KEY = 'theme';
var docEl = document.documentElement; var docEl = document.documentElement;
var mql = window.matchMedia('(prefers-color-scheme: dark)'); var mql = window.matchMedia('(prefers-color-scheme: dark)');
function getStored() { try { return localStorage.getItem(STORAGE_KEY); } catch (e) { return null; } } function getStored() { try { return localStorage.getItem(STORAGE_KEY); } catch (e) { return null; } }
function setStored(v) { try { localStorage.setItem(STORAGE_KEY, v); } catch (e) {} } function setStored(v) { try { localStorage.setItem(STORAGE_KEY, v); } catch (e) {} }
function clearStored() { try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} } function clearStored() { try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} }
function systemTheme() { return mql.matches ? 'dark' : 'light'; } function systemTheme() { return mql.matches ? 'dark' : 'light'; }
function applyTheme(theme) { function applyTheme(theme) {
docEl.setAttribute('data-bs-theme', theme); docEl.setAttribute('data-bs-theme', theme);
docEl.setAttribute('data-aria-theme', theme); docEl.setAttribute('data-aria-theme', theme);
var meta = document.querySelector('meta[name="theme-color"]'); var meta = document.querySelector('meta[name="theme-color"]');
if (meta) { if (meta) {
meta.setAttribute('content', theme === 'dark' ? '#0f1115' : '#ffffff'); meta.setAttribute('content', theme === 'dark' ? '#0f1115' : '#ffffff');
} }
var sw = document.getElementById('mokoThemeSwitch'); var sw = document.getElementById('mokoThemeSwitch');
if (sw) { if (sw) {
sw.setAttribute('aria-checked', theme === 'dark' ? 'true' : 'false'); sw.setAttribute('aria-checked', theme === 'dark' ? 'true' : 'false');
} }
} }
function initTheme() { function initTheme() {
var stored = getStored(); var stored = getStored();
applyTheme(stored ? stored : systemTheme()); applyTheme(stored ? stored : systemTheme());
} }
function posClassFromBody() { function posClassFromBody() {
var pos = (document.body.getAttribute('data-theme-fab-pos') || 'br').toLowerCase(); var pos = (document.body.getAttribute('data-theme-fab-pos') || 'br').toLowerCase();
if (!/^(br|bl|tr|tl)$/.test(pos)) pos = 'br'; if (!/^(br|bl|tr|tl)$/.test(pos)) pos = 'br';
return 'pos-' + pos; return 'pos-' + pos;
} }
function buildToggle() { function buildToggle() {
if (document.getElementById('mokoThemeFab')) return; if (document.getElementById('mokoThemeFab')) return;
var wrap = document.createElement('div'); var wrap = document.createElement('div');
wrap.id = 'mokoThemeFab'; wrap.id = 'mokoThemeFab';
wrap.className = posClassFromBody(); wrap.className = posClassFromBody();
// Light label // Light label
var lblL = document.createElement('span'); var lblL = document.createElement('span');
lblL.className = 'label'; lblL.className = 'label';
lblL.textContent = 'Light'; lblL.textContent = 'Light';
// Switch // Switch
var switchWrap = document.createElement('button'); var switchWrap = document.createElement('button');
switchWrap.id = 'mokoThemeSwitch'; switchWrap.id = 'mokoThemeSwitch';
switchWrap.type = 'button'; switchWrap.type = 'button';
switchWrap.setAttribute('role', 'switch'); switchWrap.setAttribute('role', 'switch');
switchWrap.setAttribute('aria-label', 'Toggle dark mode'); switchWrap.setAttribute('aria-label', 'Toggle dark mode');
switchWrap.setAttribute('aria-checked', 'false'); // updated after init switchWrap.setAttribute('aria-checked', 'false'); // updated after init
var track = document.createElement('span'); var track = document.createElement('span');
track.className = 'switch'; track.className = 'switch';
var knob = document.createElement('span'); var knob = document.createElement('span');
knob.className = 'knob'; knob.className = 'knob';
track.appendChild(knob); track.appendChild(knob);
switchWrap.appendChild(track); switchWrap.appendChild(track);
// Dark label // Dark label
var lblD = document.createElement('span'); var lblD = document.createElement('span');
lblD.className = 'label'; lblD.className = 'label';
lblD.textContent = 'Dark'; lblD.textContent = 'Dark';
// Auto button // Auto button
var auto = document.createElement('button'); var auto = document.createElement('button');
auto.id = 'mokoThemeAuto'; auto.id = 'mokoThemeAuto';
auto.type = 'button'; auto.type = 'button';
auto.className = 'btn btn-sm btn-link text-decoration-none px-2'; auto.className = 'btn btn-sm btn-link text-decoration-none px-2';
auto.setAttribute('aria-label', 'Follow system theme'); auto.setAttribute('aria-label', 'Follow system theme');
auto.textContent = 'Auto'; auto.textContent = 'Auto';
// Behavior // Behavior
switchWrap.addEventListener('click', function () { switchWrap.addEventListener('click', function () {
var current = (docEl.getAttribute('data-bs-theme') || 'light').toLowerCase(); var current = (docEl.getAttribute('data-bs-theme') || 'light').toLowerCase();
var next = current === 'dark' ? 'light' : 'dark'; var next = current === 'dark' ? 'light' : 'dark';
applyTheme(next); applyTheme(next);
setStored(next); setStored(next);
}); });
auto.addEventListener('click', function () { auto.addEventListener('click', function () {
clearStored(); clearStored();
applyTheme(systemTheme()); applyTheme(systemTheme());
}); });
// Respond to OS changes only when not user-forced // Respond to OS changes only when not user-forced
var onMql = function () { var onMql = function () {
if (!getStored()) applyTheme(systemTheme()); if (!getStored()) applyTheme(systemTheme());
}; };
if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onMql); if (typeof mql.addEventListener === 'function') mql.addEventListener('change', onMql);
else if (typeof mql.addListener === 'function') mql.addListener(onMql); else if (typeof mql.addListener === 'function') mql.addListener(onMql);
// Initial state // Initial state
var initial = getStored() || systemTheme(); var initial = getStored() || systemTheme();
switchWrap.setAttribute('aria-checked', initial === 'dark' ? 'true' : 'false'); switchWrap.setAttribute('aria-checked', initial === 'dark' ? 'true' : 'false');
// Mount // Mount
wrap.appendChild(lblL); wrap.appendChild(lblL);
wrap.appendChild(switchWrap); wrap.appendChild(switchWrap);
wrap.appendChild(lblD); wrap.appendChild(lblD);
wrap.appendChild(auto); wrap.appendChild(auto);
document.body.appendChild(wrap); document.body.appendChild(wrap);
// Debug helper // Debug helper
window.mokoThemeFabStatus = function () { window.mokoThemeFabStatus = function () {
var el = document.getElementById('mokoThemeFab'); var el = document.getElementById('mokoThemeFab');
if (!el) return { mounted: false }; if (!el) return { mounted: false };
var r = el.getBoundingClientRect(); var r = el.getBoundingClientRect();
return { return {
mounted: true, mounted: true,
rect: { top: r.top, left: r.left, width: r.width, height: r.height }, rect: { top: r.top, left: r.left, width: r.width, height: r.height },
zIndex: window.getComputedStyle(el).zIndex, zIndex: window.getComputedStyle(el).zIndex,
posClass: el.className posClass: el.className
}; };
}; };
// Outline if invisible // Outline if invisible
setTimeout(function () { setTimeout(function () {
var r = wrap.getBoundingClientRect(); var r = wrap.getBoundingClientRect();
if (r.width < 10 || r.height < 10) { if (r.width < 10 || r.height < 10) {
wrap.classList.add('debug-outline'); wrap.classList.add('debug-outline');
console.warn('[moko] Theme FAB mounted but appears too small — check CSS collisions.'); console.warn('[moko] Theme FAB mounted but appears too small — check CSS collisions.');
} }
}, 50); }, 50);
} }
function init() { function init() {
initTheme(); initTheme();
buildToggle(); buildToggle();
} }
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init); document.addEventListener('DOMContentLoaded', init);
} else { } else {
init(); init();
} }
})(); })();

View File

@@ -0,0 +1,347 @@
/*
=========================================================================
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
This file is part of a Moko Consulting project.
SPDX-License-Identifier: GPL-3.0-or-later
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
along with this program. If not, see https://www.gnu.org/licenses/ .
=========================================================================
FILE INFORMATION
DEFGROUP: Joomla Template
FILE: media/templates/site/moko-cassiopeia/js/gtm.js
HEADER VERSION: 1.0
VERSION: 2.0
BRIEF: Safe, configurable Google Tag Manager loader for Moko-Cassiopeia.
PATH: media/templates/site/moko-cassiopeia/js/gtm.js
NOTE: Place the <noscript> fallback iframe in your HTML template (index.php). A JS file
cannot provide a true no-JS fallback by definition.
VARIABLES:
- window.MOKO_GTM_ID (string) // Optional global GTM container ID (e.g., "GTM-XXXXXXX")
- window.MOKO_GTM_OPTIONS (object) // Optional global options (see JSDoc below)
- data- attributes on the script tag or <html>/<body>:
data-gtm-id, data-data-layer, data-debug, data-ignore-dnt,
data-env-auth, data-env-preview, data-block-on-dev
=========================================================================
*/
/* global window, document, navigator */
(() => {
"use strict";
/**
* @typedef {Object} MokoGtmOptions
* @property {string} [id] GTM container ID (e.g., "GTM-XXXXXXX")
* @property {string} [dataLayerName] Custom dataLayer name (default: "dataLayer")
* @property {boolean} [debug] Log debug messages to console (default: false)
* @property {boolean} [ignoreDNT] Ignore Do Not Track and always load (default: false)
* @property {boolean} [blockOnDev] Block loading on localhost/*.test/127.0.0.1 (default: true)
* @property {string} [envAuth] GTM Environment auth string (optional)
* @property {string} [envPreview] GTM Environment preview name (optional)
* @property {Record<string,'granted'|'denied'>} [consentDefault]
* Default Consent Mode v2 map. Keys like:
* analytics_storage, ad_storage, ad_user_data, ad_personalization, functionality_storage, security_storage
* (default: {analytics_storage:'granted', functionality_storage:'granted', security_storage:'granted'})
* @property {() => (Record<string, any>|void)} [pageVars]
* Function returning extra page variables to push on init (optional)
*/
const PKG = "moko-gtm";
const PREFIX = `[${PKG}]`;
const WIN = window;
// Public API placeholder (attached to window at the end)
/** @type {{
* init: (opts?: Partial<MokoGtmOptions>) => void,
* setConsent: (updates: Record<string,'granted'|'denied'>) => void,
* push: (...args:any[]) => void,
* isLoaded: () => boolean,
* config: () => Required<MokoGtmOptions>
* }} */
const API = {};
// ---- Utilities ---------------------------------------------------------
const isDevHost = () => {
const h = WIN.location && WIN.location.hostname || "";
return (
h === "localhost" ||
h === "127.0.0.1" ||
h.endsWith(".local") ||
h.endsWith(".test")
);
};
const dntEnabled = () => {
// Different browsers expose DNT differently; treat "1" or "yes" as enabled.
const n = navigator;
const v = (n.doNotTrack || n.msDoNotTrack || (n.navigator && n.navigator.doNotTrack) || "").toString().toLowerCase();
return v === "1" || v === "yes";
};
const getCurrentScript = () => {
// document.currentScript is best; fallback to last <script> whose src ends with /gtm.js
const cs = document.currentScript;
if (cs) return cs;
const scripts = Array.from(document.getElementsByTagName("script"));
return scripts.reverse().find(s => (s.getAttribute("src") || "").includes("/gtm.js")) || null;
};
const getAttr = (el, name) => el ? el.getAttribute(name) : null;
const readDatasetCascade = (name) => {
// Check <script>, <html>, <body>, then <meta name="moko:gtm-<name>">
const script = getCurrentScript();
const html = document.documentElement;
const body = document.body;
const meta = document.querySelector(`meta[name="moko:gtm-${name}"]`);
return (
(script && script.dataset && script.dataset[name]) ||
(html && html.dataset && html.dataset[name]) ||
(body && body.dataset && body.dataset[name]) ||
(meta && meta.getAttribute("content")) ||
null
);
};
const parseBool = (v, fallback = false) => {
if (v == null) return fallback;
const s = String(v).trim().toLowerCase();
if (["1","true","yes","y","on"].includes(s)) return true;
if (["0","false","no","n","off"].includes(s)) return false;
return fallback;
};
const debugLog = (...args) => {
if (STATE.debug) {
try { console.info(PREFIX, ...args); } catch (_) {}
}
};
// ---- Configuration & State --------------------------------------------
/** @type {Required<MokoGtmOptions>} */
const STATE = {
id: "",
dataLayerName: "dataLayer",
debug: false,
ignoreDNT: false,
blockOnDev: true,
envAuth: "",
envPreview: "",
consentDefault: {
analytics_storage: "granted",
functionality_storage: "granted",
security_storage: "granted",
// The following default to "denied" unless the site explicitly opts-in:
ad_storage: "denied",
ad_user_data: "denied",
ad_personalization: "denied",
},
pageVars: () => ({})
};
const mergeOptions = (base, extra = {}) => {
const out = {...base};
for (const k in extra) {
if (!Object.prototype.hasOwnProperty.call(extra, k)) continue;
const v = extra[k];
if (v && typeof v === "object" && !Array.isArray(v)) {
out[k] = {...(out[k] || {}), ...v};
} else if (v !== undefined) {
out[k] = v;
}
}
return out;
};
const detectOptions = () => {
// 1) Global window options
/** @type {Partial<MokoGtmOptions>} */
const globalOpts = (WIN.MOKO_GTM_OPTIONS && typeof WIN.MOKO_GTM_OPTIONS === "object") ? WIN.MOKO_GTM_OPTIONS : {};
// 2) Dataset / meta
const idFromData = readDatasetCascade("id") || WIN.MOKO_GTM_ID || "";
const dlFromData = readDatasetCascade("dataLayer") || "";
const dbgFromData = readDatasetCascade("debug");
const dntFromData = readDatasetCascade("ignoreDnt");
const devFromData = readDatasetCascade("blockOnDev");
const authFromData = readDatasetCascade("envAuth") || "";
const prevFromData = readDatasetCascade("envPreview") || "";
// 3) Combine
/** @type {Partial<MokoGtmOptions>} */
const detected = {
id: idFromData || globalOpts.id || "",
dataLayerName: dlFromData || globalOpts.dataLayerName || undefined,
debug: parseBool(dbgFromData, !!globalOpts.debug),
ignoreDNT: parseBool(dntFromData, !!globalOpts.ignoreDNT),
blockOnDev: parseBool(devFromData, (globalOpts.blockOnDev ?? true)),
envAuth: authFromData || globalOpts.envAuth || "",
envPreview: prevFromData || globalOpts.envPreview || "",
consentDefault: globalOpts.consentDefault || undefined,
pageVars: typeof globalOpts.pageVars === "function" ? globalOpts.pageVars : undefined
};
return detected;
};
// ---- dataLayer / gtag helpers -----------------------------------------
const ensureDataLayer = () => {
const l = STATE.dataLayerName;
WIN[l] = WIN[l] || [];
return WIN[l];
};
/** gtag wrapper backed by dataLayer. */
const gtag = (...args) => {
const dl = ensureDataLayer();
dl.push(arguments.length > 1 ? args : args[0]);
debugLog("gtag push:", args);
};
API.push = (...args) => gtag(...args);
API.setConsent = (updates) => {
gtag("consent", "update", updates || {});
};
API.isLoaded = () => {
const hasScript = !!document.querySelector('script[src*="googletagmanager.com/gtm.js"]');
return hasScript;
};
API.config = () => ({...STATE});
// ---- Loader ------------------------------------------------------------
const buildEnvQuery = () => {
const qp = [];
if (STATE.envAuth) qp.push(`gtm_auth=${encodeURIComponent(STATE.envAuth)}`);
if (STATE.envPreview) qp.push(`gtm_preview=${encodeURIComponent(STATE.envPreview)}`, "gtm_cookies_win=x");
return qp.length ? `&${qp.join("&")}` : "";
};
const injectScript = () => {
if (!STATE.id) {
debugLog("GTM ID missing; aborting load.");
return;
}
if (API.isLoaded()) {
debugLog("GTM already loaded; skipping duplicate injection.");
return;
}
// Standard GTM bootstrap timing event
const dl = ensureDataLayer();
dl.push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
const f = document.getElementsByTagName("script")[0];
const j = document.createElement("script");
j.async = true;
j.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(STATE.id)}${STATE.dataLayerName !== "dataLayer" ? `&l=${encodeURIComponent(STATE.dataLayerName)}` : ""}${buildEnvQuery()}`;
if (f && f.parentNode) {
f.parentNode.insertBefore(j, f);
} else {
(document.head || document.documentElement).appendChild(j);
}
debugLog("Injected GTM script:", j.src);
};
const applyDefaultConsent = () => {
// Consent Mode v2 default
gtag("consent", "default", STATE.consentDefault);
debugLog("Applied default consent:", STATE.consentDefault);
};
const pushInitialVars = () => {
// Minimal page vars; allow site to add more via pageVars()
const vars = {
event: "moko.page_init",
page_title: document.title || "",
page_language: (document.documentElement && document.documentElement.lang) || "",
...(typeof STATE.pageVars === "function" ? (STATE.pageVars() || {}) : {})
};
gtag(vars);
};
const shouldLoad = () => {
if (!STATE.ignoreDNT && dntEnabled()) {
debugLog("DNT is enabled; blocking GTM load (set ignoreDNT=true to override).");
return false;
}
if (STATE.blockOnDev && isDevHost()) {
debugLog("Development host detected; blocking GTM load (set blockOnDev=false to override).");
return false;
}
return true;
};
// ---- Public init -------------------------------------------------------
API.init = (opts = {}) => {
// Merge: defaults <- detected <- passed opts
const detected = detectOptions();
const merged = mergeOptions(STATE, mergeOptions(detected, opts));
// Commit back to STATE
Object.assign(STATE, merged);
debugLog("Config:", STATE);
// Prepare dataLayer/gtag and consent
ensureDataLayer();
applyDefaultConsent();
pushInitialVars();
// Load GTM if allowed
if (shouldLoad()) {
injectScript();
} else {
debugLog("GTM load prevented by configuration or environment.");
}
};
// ---- Auto-init on DOMContentLoaded (safe even if deferred) -------------
const autoInit = () => {
// Only auto-init if we have some ID from globals/datasets.
const detected = detectOptions();
const hasId = !!(detected.id || WIN.MOKO_GTM_ID);
if (hasId) {
API.init(); // use detected/global defaults
} else {
debugLog("No GTM ID detected; awaiting manual init via window.mokoGTM.init({ id: 'GTM-XXXXXXX' }).");
}
};
if (document.readyState === "complete" || document.readyState === "interactive") {
// Defer to ensure <body> exists for any late consumers.
setTimeout(autoInit, 0);
} else {
document.addEventListener("DOMContentLoaded", autoInit, { once: true });
}
// Expose API
WIN.mokoGTM = API;
// Helpful console hint (only if debug true after detection)
try {
const detected = detectOptions();
if (parseBool(detected.debug, false)) {
STATE.debug = true;
debugLog("Ready. You can call window.mokoGTM.init({ id: 'GTM-XXXXXXX' }).");
}
} catch (_) {}
})();

View File

@@ -1,11 +0,0 @@
/**
* @package Joomla.Site
* @subpackage Templates.Moko-Cassiopeia
* @file /media/templates/site/moko-cassiopeia/js/mod_gabble.js
* @copyright (C) 2025 Jonathan Miler || Moko Consulting <https://mokoconsulting.tech>
* @website: https://mokoconsulting.tech
* @email: hello@mokoconsulting.tech
* @phone: +1 (931) 279-6313
* @license GNU General Public License version 2 or later; see LICENSE.txt
* @note 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.
*/

View File

@@ -1,13 +1,30 @@
/** /* =========================================================================
* @package Joomla.Site * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @subpackage Templates.Moko-Cassiopeia *
* @file /media/templates/site/moko-cassiopeia/js/mod_menu/menu-metismenu-es5.js * This file is part of a Moko Consulting project.
* @copyright (C) 2025 Jonathan Miler || Moko Consulting <https://mokoconsulting.tech> *
* @website: https://mokoconsulting.tech * SPDX-License-Identifier: GPL-3.0-or-later
* @email: hello@mokoconsulting.tech *
* @phone: +1 (931) 279-6313 * This program is free software; you can redistribute it and/or modify
* @license GNU General Public License version 2 or later; see LICENSE.txt * it under the terms of the GNU General Public License as published by
* @note 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. * 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/js/mod_menu/menu-metismenu-es5.js
* VERSION: 02.00
* BRIEF: ES5-compatible MetisMenu script for Joomla mod_menu in Moko-Cassiopeia
* =========================================================================
*/ */
(function () { (function () {

View File

@@ -1,13 +1,30 @@
/** /* =========================================================================
* @package Joomla.Site * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
* @subpackage Templates.Moko-Cassiopeia *
* @file /media/templates/site/moko-cassiopeia/js/mod_menu/menu-metismenu.js * This file is part of a Moko Consulting project.
* @copyright (C) 2025 Jonathan Miler || Moko Consulting <https://mokoconsulting.tech> *
* @website: https://mokoconsulting.tech * SPDX-License-Identifier: GPL-3.0-or-later
* @email: hello@mokoconsulting.tech *
* @phone: +1 (931) 279-6313 * This program is free software; you can redistribute it and/or modify
* @license GNU General Public License version 2 or later; see LICENSE.txt * it under the terms of the GNU General Public License as published by
* @note 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. * 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/js/mod_menu/menu-metismenu.js
* VERSION: 02.00
* BRIEF: Modern MetisMenu script for Joomla mod_menu in Moko-Cassiopeia
* =========================================================================
*/ */
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {

View File

@@ -1,25 +1,14 @@
/** /* =========================================================================
* template.js — Custom JavaScript for the Moko Cassiopeia Joomla template * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* @package Joomla.Site
* @subpackage Templates.Moko-Cassiopeia
* @file /media/templates/site/moko-cassiopeia/js/template.js
* @version 2.0
*
* @copyright (C) 2025 Moko Consulting
* @author Jonathan Miller
* @website https://mokoconsulting.tech
* @email hello@mokoconsulting.tech
* @phone +1 (931) 279-6313
*
* SPDX-License-Identifier: GPL-3.0-or-later
* *
* This file is part of a Moko Consulting project. * This file is part of a Moko Consulting project.
* *
* This program is free software: you can redistribute it and/or modify it * SPDX-License-Identifier: GPL-3.0-or-later
* under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or (at * This program is free software; you can redistribute it and/or modify
* your option) any later version. * 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, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -27,7 +16,15 @@
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/js/template.js
* VERSION: 02.00
* BRIEF: Core JavaScript utilities and behaviors for Moko-Cassiopeia template
* =========================================================================
*/ */
(function (win, doc) { (function (win, doc) {

View File

@@ -1,25 +1,14 @@
/** /* =========================================================================
* theme-init.js — Light/Dark mode initialization for Moko Cassiopeia * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* @package Joomla.Site
* @subpackage Templates.Moko-Cassiopeia
* @file /media/templates/site/moko-cassiopeia/js/theme-init.js
* @version 2.0
*
* @copyright (C) 2025 Moko Consulting
* @author Jonathan Miller
* @website https://mokoconsulting.tech
* @email hello@mokoconsulting.tech
* @phone +1 (931) 279-6313
*
* SPDX-License-Identifier: GPL-3.0-or-later
* *
* This file is part of a Moko Consulting project. * This file is part of a Moko Consulting project.
* *
* This program is free software: you can redistribute it and/or modify it * SPDX-License-Identifier: GPL-3.0-or-later
* under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 3 of the License, or (at * This program is free software; you can redistribute it and/or modify
* your option) any later version. * 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, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -27,7 +16,15 @@
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/js/theme-init.js
* VERSION: 02.00
* BRIEF: Initialization script for Moko-Cassiopeia theme features and behaviors
* =========================================================================
*/ */
(function (win, doc) { (function (win, doc) {

View File

@@ -1,25 +1,14 @@
/** /* =========================================================================
* user.js — User Custom JS File for Moko Cassiopeia * Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* @package Joomla.Site
* @subpackage Templates.Moko-Cassiopeia
* @file /media/templates/site/moko-cassiopeia/js/user.js
* @version 2.0
*
* @copyright (C) 2025 Moko Consulting
* @author Jonathan Miller
* @website https://mokoconsulting.tech
* @email hello@mokoconsulting.tech
* @phone +1 (931) 279-6313
*
* SPDX-License-Identifier: GPL-3.0-or-later
* *
* This file is part of a Moko Consulting project. * This file is part of a Moko Consulting project.
* *
* This program is free software: you can redistribute it and/or modify it * SPDX-License-Identifier: GPL-3.0-or-later
* under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 3 of the License, or (at * This program is free software; you can redistribute it and/or modify
* your option) any later version. * 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, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -27,5 +16,13 @@
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: media/templates/site/moko-cassiopeia/js/user.js
* VERSION: 02.00
* BRIEF: JavaScript for handling user-specific interactions in Moko-Cassiopeia template
* =========================================================================
*/ */

View File

@@ -0,0 +1,108 @@
<?php
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: templates/moko-cassiopeia/component.php
* VERSION: 02.00
* BRIEF: Minimal component-only template file for Moko-Cassiopeia
* =========================================================================
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
/** @var Joomla\CMS\Document\HtmlDocument $this */
$app = Factory::getApplication();
$wa = $this->getWebAssetManager();
// Color Theme
$paramsColorName = $this->params->get('colorName', 'colors_standard');
$assetColorName = 'theme.' . $paramsColorName;
$wa->registerAndUseStyle($assetColorName, 'media/templates/site/moko-cassiopeia/css/global/' . $paramsColorName . '.css');
// Use a font scheme if set in the template style options
$paramsFontScheme = $this->params->get('useFontScheme', false);
$fontStyles = '';
if ($paramsFontScheme) {
if (stripos($paramsFontScheme, 'https://') === 0) {
$this->getPreloadManager()->preconnect('https://fonts.googleapis.com/', ['crossorigin' => 'anonymous']);
$this->getPreloadManager()->preconnect('https://fonts.gstatic.com/', ['crossorigin' => 'anonymous']);
$this->getPreloadManager()->preload($paramsFontScheme, ['as' => 'style', 'crossorigin' => 'anonymous']);
$wa->registerAndUseStyle('fontscheme.current', $paramsFontScheme, [], ['media' => 'print', 'rel' => 'lazy-stylesheet', 'onload' => 'this.media=\'all\'', 'crossorigin' => 'anonymous']);
if (preg_match_all('/family=([^?:]*):/i', $paramsFontScheme, $matches) > 0) {
$fontStyles = '--font-family-body: "' . str_replace('+', ' ', $matches[1][0]) . '", sans-serif;
--font-family-headings: "' . str_replace('+', ' ', isset($matches[1][1]) ? $matches[1][1] : $matches[1][0]) . '", sans-serif;
--font-weight-normal: 400;
--font-weight-headings: 700;';
}
} else {
$wa->registerAndUseStyle('fontscheme.current', $paramsFontScheme, ['version' => 'auto'], ['media' => 'print', 'rel' => 'lazy-stylesheet', 'onload' => 'this.media=\'all\'']);
$this->getPreloadManager()->preload($wa->getAsset('style', 'fontscheme.current')->getUri() . '?' . $this->getMediaVersion(), ['as' => 'style']);
}
}
// Enable assets
$wa->usePreset('template.moko-cassiopeia.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr'))
->useStyle('template.active.language')
->useStyle('template.user')
->useScript('template.user')
->addInlineStyle(":root {
--hue: 214;
--template-bg-light: #f0f4fb;
--template-text-dark: #495057;
--template-text-light: #ffffff;
--template-link-color: #2a69b8;
--template-special-color: #001B4C;
$fontStyles
}");
// Override 'template.active' asset to set correct ltr/rtl dependency
$wa->registerStyle('template.active', '', [], [], ['template.moko-cassiopeia.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')]);
// Browsers support SVG favicons
$this->addHeadLink(HTMLHelper::_('image', 'joomla-favicon.svg', '', [], true, 1), 'icon', 'rel', ['type' => 'image/svg+xml']);
$this->addHeadLink(HTMLHelper::_('image', 'favicon.ico', '', [], true, 1), 'alternate icon', 'rel', ['type' => 'image/vnd.microsoft.icon']);
$this->addHeadLink(HTMLHelper::_('image', 'joomla-favicon-pinned.svg', '', [], true, 1), 'mask-icon', 'rel', ['color' => '#000']);
// Defer font awesome
$wa->getAsset('style', 'fontawesome')->setAttribute('rel', 'lazy-stylesheet');
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<jdoc:include type="metas" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<jdoc:include type="styles" />
<jdoc:include type="scripts" />
</head>
<body class="<?php echo $this->direction === 'rtl' ? 'rtl' : ''; ?>">
<jdoc:include type="message" />
<jdoc:include type="component" />
</body>
</html>

View File

@@ -0,0 +1,42 @@
<?php
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: templates/moko-cassiopeia/custom.php
* VERSION: 02.00
* BRIEF: Custom entry template file for Moko-Cassiopeia with user-defined overrides
* =========================================================================
*/
function console_log($output, $with_script_tags = true) {
$js_code = 'console.log(' . json_encode($output, JSON_HEX_TAG) .
');';
if ($with_script_tags) {
$js_code = '<script>' . $js_code . '</script>';
}
echo $js_code;
}
?>
<!--
Custom code included here
-->

View File

@@ -0,0 +1,279 @@
<?php
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: templates/moko-cassiopeia/error.php
* VERSION: 02.00
* BRIEF: Error page template file for Moko-Cassiopeia
* =========================================================================
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
/** @var Joomla\CMS\Document\ErrorDocument|Joomla\CMS\Document\HtmlDocument $this */
$app = Factory::getApplication();
$params = $this->params;
$wa = $this->getWebAssetManager();
// ------------------ Params ------------------
$colorLight = (string) $params->get('colorLightName', 'colors_standard');
$colorDark = (string) $params->get('colorDarkName', 'colors_standard');
$themeFab = (int) $params->get('theme_fab_enabled', 1);
$fABodyPos = (string) $params->get('theme_fab_pos', 'br');
$gtmEnabled = (int) $params->get('googletagmanager', 0);
$gtmId = (string) $params->get('googletagmanagerid', '');
$fa6KitCode = (string) $params->get('fA6KitCode', '');
$stickyHeader = (bool) $params->get('stickyHeader', 0);
$brandEnabled = (int) $params->get('brand', 1);
$siteDescription = (string) $params->get('siteDescription', '');
// Drawer icon params (escaped)
$params_leftIcon = htmlspecialchars($params->get('drawerLeftIcon', 'fa-solid fa-chevron-right'), ENT_QUOTES, 'UTF-8');
$params_rightIcon = htmlspecialchars($params->get('drawerRightIcon', 'fa-solid fa-chevron-left'), ENT_QUOTES, 'UTF-8');
// ------------------ Styles ------------------
$wa->useStyle('template.base');
$wa->useStyle('template.user');
$wa->useStyle('vendor.vmbasic');
$wa->useStyle('vendor.gable');
// Light/Dark variable sheets (load before consumers)
if ($wa->assetExists('style', 'template.light.' . $colorLight)) {
$wa->useStyle('template.light.' . $colorLight);
}
if ($wa->assetExists('style', 'template.dark.' . $colorDark)) {
$wa->useStyle('template.dark.' . $colorDark);
}
// ------------------ Scripts ------------------
$wa->useScript('theme-init.js');
if ($themeFab === 1) {
$wa->useScript('darkmode-toggle.js');
}
if ($gtmEnabled === 1) {
$wa->useScript('gtm.js');
}
// Optional Font Awesome 6 Kit (preferred) or FA5 fallback
if (!empty($fa6KitCode)) {
HTMLHelper::_('script', 'https://kit.fontawesome.com/' . rawurlencode($fa6KitCode) . '.js', [
'crossorigin' => 'anonymous'
]);
} else {
HTMLHelper::_('stylesheet', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css', ['version' => 'auto'], [
'crossorigin' => 'anonymous',
'referrerpolicy' => 'no-referrer',
]);
}
// ------------------ Context (logo, bootstrap needs) ------------------
$sitename = htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8');
// Build logo/title
if ($params->get('logoFile')) {
$logo = HTMLHelper::_(
'image',
Uri::root(false) . htmlspecialchars($params->get('logoFile'), ENT_QUOTES),
$sitename,
['loading' => 'eager', 'decoding' => 'async'],
false,
0
);
} elseif ($params->get('siteTitle')) {
$logo = '<span title="' . $sitename . '">' . htmlspecialchars($params->get('siteTitle'), ENT_COMPAT, 'UTF-8') . '</span>';
} else {
$logo = HTMLHelper::_('image', 'full_logo.png', $sitename, ['class' => 'logo d-inline-block', 'loading' => 'eager', 'decoding' => 'async'], true, 0);
}
// ------------------ Error details ------------------
$errorObj = isset($this->error) && is_object($this->error) ? $this->error : null;
$errorCode = $errorObj ? (int) $errorObj->getCode() : 500;
$errorMsg = $errorObj ? $errorObj->getMessage() : Text::_('JERROR_AN_ERROR_HAS_OCCURRED');
$debugOn = defined('JDEBUG') && JDEBUG;
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#ffffff" id="meta-theme-color" />
<jdoc:include type="head" />
</head>
<body data-theme-fab-pos="<?php echo htmlspecialchars($fABodyPos, ENT_QUOTES, 'UTF-8'); ?>">
<?php if ($gtmEnabled === 1 && !empty($gtmId)) : ?>
<!-- Google Tag Manager (noscript) -->
<noscript>
<iframe src="https://www.googletagmanager.com/ns.html?id=<?php echo htmlspecialchars($gtmId, ENT_QUOTES, 'UTF-8'); ?>"
height="0" width="0" style="display:none;visibility:hidden"></iframe>
</noscript>
<!-- End Google Tag Manager (noscript) -->
<?php endif; ?>
<!-- ========== DUPLICATED HEADER FROM INDEX ========== -->
<header class="header container-header full-width<?php echo $stickyHeader ? ' position-sticky sticky-top' : ''; ?>">
<?php if ($this->countModules('topbar')) : ?>
<div class="container-topbar">
<jdoc:include type="modules" name="topbar" style="none" />
</div>
<?php endif; ?>
<div class="header-top">
<?php if ($this->countModules('below-topbar')) : ?>
<div class="grid-child container-below-topbar">
<jdoc:include type="modules" name="below-topbar" style="none" />
</div>
<?php endif; ?>
<?php if ($brandEnabled) : ?>
<div class="grid-child">
<div class="navbar-brand">
<a class="brand-logo" href="<?php echo $this->baseurl; ?>/">
<?php echo $logo; ?>
</a>
<?php if (!empty($siteDescription)) : ?>
<div class="site-description"><?php echo htmlspecialchars($siteDescription); ?></div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php if ($this->countModules('below-logo')) : ?>
<div class="grid container-below-logo">
<jdoc:include type="modules" name="below-logo" style="none" />
</div>
<?php endif; ?>
</div>
<?php if ($this->countModules('menu', true) || $this->countModules('search', true)) : ?>
<div class="grid-child container-nav">
<?php if ($this->countModules('menu', true)) : ?>
<jdoc:include type="modules" name="menu" style="none" />
<?php endif; ?>
<?php if ($this->countModules('search', true)) : ?>
<div class="container-search">
<jdoc:include type="modules" name="search" style="none" />
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</header>
<!-- ========== END DUPLICATED HEADER ========== -->
<main class="container my-4">
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<h1 class="h3">
<span class="text-muted"><?php echo Text::_('JERROR_LAYOUT_ERROR_HAS_OCCURRED'); ?>:</span>
<strong><?php echo (int) $errorCode; ?></strong>
</h1>
<p class="lead mb-1">
<?php echo htmlspecialchars($errorMsg, ENT_QUOTES, 'UTF-8'); ?>
</p>
<p class="text-muted mb-0">
<?php echo Text::_('JERROR_LAYOUT_PLEASE_TRY_ONE_OF_THE_FOLLOWING_PAGES'); ?>
</p>
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-primary" href="<?php echo htmlspecialchars(Uri::base(), ENT_QUOTES, 'UTF-8'); ?>">
<i class="fas fa-home me-1" aria-hidden="true"></i>
<?php echo Text::_('JERROR_LAYOUT_HOME_PAGE'); ?>
</a>
<button class="btn btn-outline-secondary" type="button" onclick="history.back();">
<i class="fas fa-arrow-left me-1" aria-hidden="true"></i>
<?php echo Text::_('JPREV'); ?>
</button>
</div>
<?php if ($debugOn && $errorObj) : ?>
<section class="mt-4" role="region" aria-label="Debug Details">
<div class="alert alert-warning"><strong>Debug mode is ON</strong> — detailed error information is shown below.</div>
<div class="card mb-3">
<div class="card-header fw-bold">Exception</div>
<div class="card-body small">
<dl class="row mb-0">
<dt class="col-sm-3">Class</dt>
<dd class="col-sm-9"><?php echo htmlspecialchars(get_class($errorObj), ENT_QUOTES, 'UTF-8'); ?></dd>
<dt class="col-sm-3">Code</dt>
<dd class="col-sm-9"><?php echo (int) $errorObj->getCode(); ?></dd>
<dt class="col-sm-3">Message</dt>
<dd class="col-sm-9 text-break"><?php echo htmlspecialchars($errorObj->getMessage(), ENT_QUOTES, 'UTF-8'); ?></dd>
<dt class="col-sm-3">File</dt>
<dd class="col-sm-9 text-break"><?php echo htmlspecialchars($errorObj->getFile(), ENT_QUOTES, 'UTF-8'); ?> : <?php echo (int) $errorObj->getLine(); ?></dd>
</dl>
</div>
</div>
<?php $trace = method_exists($errorObj, 'getTrace') ? $errorObj->getTrace() : []; ?>
<div class="card mb-3">
<div class="card-header fw-bold">Stack Trace (<?php echo count($trace); ?> frames)</div>
<div class="card-body small">
<?php if ($trace) : ?>
<ol class="mb-0 ps-3">
<?php foreach ($trace as $i => $frame) :
$file = $frame['file'] ?? '[internal]';
$line = isset($frame['line']) ? (int) $frame['line'] : 0;
$func = $frame['function'] ?? '';
$class= $frame['class'] ?? '';
$type = $frame['type'] ?? '';
?>
<li class="mb-2">
<div class="text-break"><code>#<?php echo $i; ?></code> <?php echo htmlspecialchars($class . $type . $func, ENT_QUOTES, 'UTF-8'); ?>()</div>
<div class="text-muted"><?php echo htmlspecialchars($file, ENT_QUOTES, 'UTF-8'); ?><?php echo $line ? ':' . $line : ''; ?></div>
</li>
<?php endforeach; ?>
</ol>
<?php else : ?>
<em>No stack trace available.</em>
<?php endif; ?>
</div>
</div>
</section>
<?php endif; ?>
</main>
<footer class="container-footer footer full-width py-4">
<?php if ($this->countModules('footer-menu', true)) : ?>
<div class="grid-child footer-menu">
<jdoc:include type="modules" name="footer-menu" />
</div>
<?php endif; ?>
<?php if ($this->countModules('footer', true)) : ?>
<div class="grid-child">
<jdoc:include type="modules" name="footer" style="none" />
</div>
<?php endif; ?>
</footer>
<jdoc:include type="modules" name="debug" style="none" />
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,177 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_contact
*
* @copyright (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Router\Route;
use Joomla\Component\Contact\Site\Helper\RouteHelper;
$tparams = $this->item->params;
$canDo = ContentHelper::getActions('com_contact', 'category', $this->item->catid);
$canEdit = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by === Factory::getUser()->id);
$htag = $tparams->get('show_page_heading') ? 'h2' : 'h1';
?>
<div class="com-contact contact" itemscope itemtype="https://schema.org/Person">
<?php if ($canEdit) : ?>
<div class="icons">
<div class="text-end">
<div>
<?php echo HTMLHelper::_('contacticon.edit', $this->item, $tparams); ?>
</div>
</div>
</div>
<?php endif; ?>
<?php if ($tparams->get('show_page_heading')) : ?>
<h1>
<?php echo $this->escape($tparams->get('page_heading')); ?>
</h1>
<?php endif; ?>
<?php if ($this->item->name && $tparams->get('show_name')) : ?>
<div class="page-header">
<<?php echo $htag; ?>>
<?php if ($this->item->published == 0) : ?>
<span class="badge bg-warning text-light"><?php echo Text::_('JUNPUBLISHED'); ?></span>
<?php endif; ?>
<span class="contact-name" itemprop="name"><?php echo $this->item->name; ?></span>
</<?php echo $htag; ?>>
</div>
<?php endif; ?>
<div class="row gy-4 mb-4">
<div class="col-md-6">
<?php $show_contact_category = $tparams->get('show_contact_category'); ?>
<?php if ($show_contact_category === 'show_no_link') : ?>
<h3>
<span class="contact-category"><?php echo $this->item->category_title; ?></span>
</h3>
<?php elseif ($show_contact_category === 'show_with_link') : ?>
<?php $contactLink = RouteHelper::getCategoryRoute($this->item->catid, $this->item->language); ?>
<h3>
<span class="contact-category"><a href="<?php echo $contactLink; ?>">
<?php echo $this->escape($this->item->category_title); ?></a>
</span>
</h3>
<?php endif; ?>
<?php echo $this->item->event->afterDisplayTitle; ?>
<?php if ($tparams->get('show_contact_list') && count($this->contacts) > 1) : ?>
<form action="#" method="get" name="selectForm" id="selectForm">
<label for="select_contact"><?php echo Text::_('COM_CONTACT_SELECT_CONTACT'); ?></label>
<?php echo HTMLHelper::_(
'select.genericlist',
$this->contacts,
'select_contact',
'class="form-select" onchange="document.location.href = this.value"',
'link',
'name',
$this->item->link
);
?>
</form>
<?php endif; ?>
<?php if ($tparams->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<div class="com-contact__tags">
<?php $this->item->tagLayout = new FileLayout('joomla.content.tags'); ?>
<?php echo $this->item->tagLayout->render($this->item->tags->itemTags); ?>
</div>
<?php endif; ?>
<?php echo $this->item->event->beforeDisplayContent; ?>
<?php if ($this->params->get('show_info', 1)) : ?>
<div class="com-contact__container">
<?php echo '<h3>' . Text::_('COM_CONTACT_DETAILS') . '</h3>'; ?>
<?php if ($this->item->image && $tparams->get('show_image')) : ?>
<div class="com-contact__thumbnail thumbnail">
<?php echo LayoutHelper::render(
'joomla.html.image',
[
'src' => $this->item->image,
'alt' => $this->item->name,
'itemprop' => 'image',
]
); ?>
</div>
<?php endif; ?>
<?php if ($this->item->con_position && $tparams->get('show_position')) : ?>
<dl class="com-contact__position contact-position dl-horizontal">
<dt><?php echo Text::_('COM_CONTACT_POSITION'); ?>:</dt>
<dd itemprop="jobTitle">
<?php echo $this->item->con_position; ?>
</dd>
</dl>
<?php endif; ?>
<div class="com-contact__info">
<?php echo $this->loadTemplate('address'); ?>
<?php if ($tparams->get('allow_vcard')) : ?>
<?php echo Text::_('COM_CONTACT_DOWNLOAD_INFORMATION_AS'); ?>
<a href="<?php echo Route::_('index.php?option=com_contact&view=contact&catid=' . $this->item->catslug . '&id=' . $this->item->slug . '&format=vcf'); ?>">
<?php echo Text::_('COM_CONTACT_VCARD'); ?></a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php if ($tparams->get('show_links')) : ?>
<?php echo $this->loadTemplate('links'); ?>
<?php endif; ?>
<?php if ($tparams->get('show_articles') && $this->item->user_id && $this->item->articles) : ?>
<?php echo '<h3>' . Text::_('JGLOBAL_ARTICLES') . '</h3>'; ?>
<?php echo $this->loadTemplate('articles'); ?>
<?php endif; ?>
<?php if ($tparams->get('show_profile') && $this->item->user_id && PluginHelper::isEnabled('user', 'profile')) : ?>
<?php echo '<h3>' . Text::_('COM_CONTACT_PROFILE') . '</h3>'; ?>
<?php echo $this->loadTemplate('profile'); ?>
<?php endif; ?>
<?php if ($tparams->get('show_user_custom_fields') && $this->contactUser) : ?>
<?php echo $this->loadTemplate('user_custom_fields'); ?>
<?php endif; ?>
</div>
<div class="col-md-6">
<?php if ($tparams->get('show_email_form') && ($this->item->email_to || $this->item->user_id)) : ?>
<?php echo $this->loadTemplate('form'); ?>
<?php endif; ?>
</div>
</div>
<?php if ($this->item->misc && $tparams->get('show_misc')) : ?>
<?php echo '<h3>' . Text::_('COM_CONTACT_OTHER_INFORMATION') . '</h3>'; ?>
<div class="com-contact__miscinfo contact-miscinfo">
<div class="contact-misc">
<?php echo $this->item->misc; ?>
</div>
</div>
<?php endif; ?>
<?php echo $this->item->event->afterDisplayContent; ?>
</div>

View File

@@ -0,0 +1,118 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,177 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_contact
*
* @copyright (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Helper\ContentHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Router\Route;
use Joomla\Component\Contact\Site\Helper\RouteHelper;
$tparams = $this->item->params;
$canDo = ContentHelper::getActions('com_contact', 'category', $this->item->catid);
$canEdit = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by === Factory::getUser()->id);
$htag = $tparams->get('show_page_heading') ? 'h2' : 'h1';
?>
<div class="com-contact contact" itemscope itemtype="https://schema.org/Person">
<?php if ($canEdit) : ?>
<div class="icons">
<div class="text-end">
<div>
<?php echo HTMLHelper::_('contacticon.edit', $this->item, $tparams); ?>
</div>
</div>
</div>
<?php endif; ?>
<?php if ($tparams->get('show_page_heading')) : ?>
<h1>
<?php echo $this->escape($tparams->get('page_heading')); ?>
</h1>
<?php endif; ?>
<?php if ($this->item->name && $tparams->get('show_name')) : ?>
<div class="page-header">
<<?php echo $htag; ?>>
<?php if ($this->item->published == 0) : ?>
<span class="badge bg-warning text-light"><?php echo Text::_('JUNPUBLISHED'); ?></span>
<?php endif; ?>
<span class="contact-name" itemprop="name"><?php echo $this->item->name; ?></span>
</<?php echo $htag; ?>>
</div>
<?php endif; ?>
<div class="row gy-4 mb-4">
<div class="col-md-6">
<?php $show_contact_category = $tparams->get('show_contact_category'); ?>
<?php if ($show_contact_category === 'show_no_link') : ?>
<h3>
<span class="contact-category"><?php echo $this->item->category_title; ?></span>
</h3>
<?php elseif ($show_contact_category === 'show_with_link') : ?>
<?php $contactLink = RouteHelper::getCategoryRoute($this->item->catid, $this->item->language); ?>
<h3>
<span class="contact-category"><a href="<?php echo $contactLink; ?>">
<?php echo $this->escape($this->item->category_title); ?></a>
</span>
</h3>
<?php endif; ?>
<?php echo $this->item->event->afterDisplayTitle; ?>
<?php if ($tparams->get('show_contact_list') && count($this->contacts) > 1) : ?>
<form action="#" method="get" name="selectForm" id="selectForm">
<label for="select_contact"><?php echo Text::_('COM_CONTACT_SELECT_CONTACT'); ?></label>
<?php echo HTMLHelper::_(
'select.genericlist',
$this->contacts,
'select_contact',
'class="form-select" onchange="document.location.href = this.value"',
'link',
'name',
$this->item->link
);
?>
</form>
<?php endif; ?>
<?php if ($tparams->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<div class="com-contact__tags">
<?php $this->item->tagLayout = new FileLayout('joomla.content.tags'); ?>
<?php echo $this->item->tagLayout->render($this->item->tags->itemTags); ?>
</div>
<?php endif; ?>
<?php echo $this->item->event->beforeDisplayContent; ?>
<?php if ($this->params->get('show_info', 1)) : ?>
<div class="com-contact__container">
<?php echo '<h3>' . Text::_('COM_CONTACT_DETAILS') . '</h3>'; ?>
<?php if ($this->item->image && $tparams->get('show_image')) : ?>
<div class="com-contact__thumbnail thumbnail">
<?php echo LayoutHelper::render(
'joomla.html.image',
[
'src' => $this->item->image,
'alt' => $this->item->name,
'itemprop' => 'image',
]
); ?>
</div>
<?php endif; ?>
<?php if ($this->item->con_position && $tparams->get('show_position')) : ?>
<dl class="com-contact__position contact-position dl-horizontal">
<dt><?php echo Text::_('COM_CONTACT_POSITION'); ?>:</dt>
<dd itemprop="jobTitle">
<?php echo $this->item->con_position; ?>
</dd>
</dl>
<?php endif; ?>
<div class="com-contact__info">
<?php echo $this->loadTemplate('address'); ?>
<?php if ($tparams->get('allow_vcard')) : ?>
<?php echo Text::_('COM_CONTACT_DOWNLOAD_INFORMATION_AS'); ?>
<a href="<?php echo Route::_('index.php?option=com_contact&view=contact&catid=' . $this->item->catslug . '&id=' . $this->item->slug . '&format=vcf'); ?>">
<?php echo Text::_('COM_CONTACT_VCARD'); ?></a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php if ($tparams->get('show_links')) : ?>
<?php echo $this->loadTemplate('links'); ?>
<?php endif; ?>
<?php if ($tparams->get('show_articles') && $this->item->user_id && $this->item->articles) : ?>
<?php echo '<h3>' . Text::_('JGLOBAL_ARTICLES') . '</h3>'; ?>
<?php echo $this->loadTemplate('articles'); ?>
<?php endif; ?>
<?php if ($tparams->get('show_profile') && $this->item->user_id && PluginHelper::isEnabled('user', 'profile')) : ?>
<?php echo '<h3>' . Text::_('COM_CONTACT_PROFILE') . '</h3>'; ?>
<?php echo $this->loadTemplate('profile'); ?>
<?php endif; ?>
<?php if ($tparams->get('show_user_custom_fields') && $this->contactUser) : ?>
<?php echo $this->loadTemplate('user_custom_fields'); ?>
<?php endif; ?>
</div>
<div class="col-md-6">
<?php if ($tparams->get('show_email_form') && ($this->item->email_to || $this->item->user_id)) : ?>
<?php echo $this->loadTemplate('form'); ?>
<?php endif; ?>
</div>
</div>
<?php if ($this->item->misc && $tparams->get('show_misc')) : ?>
<?php echo '<h3>' . Text::_('COM_CONTACT_OTHER_INFORMATION') . '</h3>'; ?>
<div class="com-contact__miscinfo contact-miscinfo">
<div class="contact-misc">
<?php echo $this->item->misc; ?>
</div>
</div>
<?php endif; ?>
<?php echo $this->item->event->afterDisplayContent; ?>
</div>

View File

@@ -0,0 +1,118 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,118 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,173 @@
<?php
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: templates/moko-cassiopeia/html/com_content/article/toc-left.php
* VERSION: 02.00
* BRIEF: Template override for Joomla articles with Table of Contents aligned left
* =========================================================================
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\Content\Administrator\Extension\ContentComponent;
use Joomla\Component\Content\Site\Helper\RouteHelper;
// Create shortcuts to some parameters.
$params = $this->item->params;
$canEdit = $params->get('access-edit');
$user = Factory::getUser();
$info = $params->get('info_block_position', 0);
$htag = $this->params->get('show_page_heading') ? 'h2' : 'h1';
// Check if associations are implemented. If they are, define the parameter.
$assocParam = (Associations::isEnabled() && $params->get('show_associations'));
$currentDate = Factory::getDate()->format('Y-m-d H:i:s');
$isNotPublishedYet = $this->item->publish_up > $currentDate;
$isExpired = !is_null($this->item->publish_down) && $this->item->publish_down < $currentDate;
?>
<div class="com-content-article item-page<?php echo $this->pageclass_sfx; ?>" itemscope itemtype="https://schema.org/Article">
<meta itemprop="inLanguage" content="<?php echo ($this->item->language === '*') ? Factory::getApplication()->get('language') : $this->item->language; ?>">
<?php if ($this->params->get('show_page_heading')) : ?>
<div class="page-header">
<h1> <?php echo $this->escape($this->params->get('page_heading')); ?> </h1>
</div>
<?php endif;
if (!empty($this->item->pagination) && !$this->item->paginationposition && $this->item->paginationrelative) {
echo $this->item->pagination;
}
?>
<?php $useDefList = $params->get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date')
|| $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam; ?>
<?php if ($params->get('show_title')) : ?>
<div class="page-header">
<<?php echo $htag; ?> itemprop="headline">
<?php echo $this->escape($this->item->title); ?>
</<?php echo $htag; ?>>
<?php if ($this->item->state == ContentComponent::CONDITION_UNPUBLISHED) : ?>
<span class="badge bg-warning text-light"><?php echo Text::_('JUNPUBLISHED'); ?></span>
<?php endif; ?>
<?php if ($isNotPublishedYet) : ?>
<span class="badge bg-warning text-light"><?php echo Text::_('JNOTPUBLISHEDYET'); ?></span>
<?php endif; ?>
<?php if ($isExpired) : ?>
<span class="badge bg-warning text-light"><?php echo Text::_('JEXPIRED'); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($canEdit) : ?>
<?php echo LayoutHelper::render('joomla.content.icons', ['params' => $params, 'item' => $this->item]); ?>
<?php endif; ?>
<?php // Content is generated by content plugin event "onContentAfterTitle" ?>
<?php echo $this->item->event->afterDisplayTitle; ?>
<?php if ($useDefList && ($info == 0 || $info == 2)) : ?>
<?php echo LayoutHelper::render('joomla.content.info_block', ['item' => $this->item, 'params' => $params, 'position' => 'above']); ?>
<?php endif; ?>
<?php if ($info == 0 && $params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php $this->item->tagLayout = new FileLayout('joomla.content.tags'); ?>
<?php echo $this->item->tagLayout->render($this->item->tags->itemTags); ?>
<?php endif; ?>
<?php // Content is generated by content plugin event "onContentBeforeDisplay" ?>
<?php echo $this->item->event->beforeDisplayContent; ?>
<?php if ((int) $params->get('urls_position', 0) === 0) : ?>
<?php echo $this->loadTemplate('links'); ?>
<?php endif; ?>
<?php if ($params->get('access-view')) : ?>
<?php echo LayoutHelper::render('joomla.content.full_image', $this->item); ?>
<?php
if (!empty($this->item->pagination) && !$this->item->paginationposition && !$this->item->paginationrelative) :
echo $this->item->pagination;
endif;
?>
<div itemprop="articleBody" class="com-content-article__body">
<div class="container-toc-left">
<?php
// Table of Contents header using template language string
echo '<h2>' . Text::_('TPL_MOKO-CASSIOPEIA_TOC') . '</h2>';
?>
<nav id="toc" data-toggle="toc"></nav>
</div>
<?php
echo $this->item->text;
?>
</div>
<?php if ($info == 1 || $info == 2) : ?>
<?php if ($useDefList) : ?>
<?php echo LayoutHelper::render('joomla.content.info_block', ['item' => $this->item, 'params' => $params, 'position' => 'below']); ?>
<?php endif; ?>
<?php if ($params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php $this->item->tagLayout = new FileLayout('joomla.content.tags'); ?>
<?php echo $this->item->tagLayout->render($this->item->tags->itemTags); ?>
<?php endif; ?>
<?php endif; ?>
<?php
if (!empty($this->item->pagination) && $this->item->paginationposition && !$this->item->paginationrelative) :
echo $this->item->pagination;
?>
<?php endif; ?>
<?php if ((int) $params->get('urls_position', 0) === 1) : ?>
<?php echo $this->loadTemplate('links'); ?>
<?php endif; ?>
<?php // Optional teaser intro text for guests ?>
<?php elseif ($params->get('show_noauth') == true && $user->get('guest')) : ?>
<?php echo LayoutHelper::render('joomla.content.intro_image', $this->item); ?>
<?php echo HTMLHelper::_('content.prepare', $this->item->introtext); ?>
<?php // Optional link to let them register to see the whole article. ?>
<?php if ($params->get('show_readmore') && $this->item->fulltext != null) : ?>
<?php $menu = Factory::getApplication()->getMenu(); ?>
<?php $active = $menu->getActive(); ?>
<?php $itemId = $active->id; ?>
<?php $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); ?>
<?php $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); ?>
<?php echo LayoutHelper::render('joomla.content.readmore', ['item' => $this->item, 'params' => $params, 'link' => $link]); ?>
<?php endif; ?>
<?php endif; ?>
<?php
if (!empty($this->item->pagination) && $this->item->paginationposition && $this->item->paginationrelative) :
echo $this->item->pagination;
?>
<?php endif; ?>
<?php // Content is generated by content plugin event "onContentAfterDisplay" ?>
<?php echo $this->item->event->afterDisplayContent; ?>
</div>

View File

@@ -0,0 +1,175 @@
<?php
/* =========================================================================
* Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* 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
* along with this program. If not, see https://www.gnu.org/licenses/ .
* =========================================================================
* FILE INFORMATION
* DEFGROUP: Joomla
* INGROUP: Moko-Cassiopeia
* PATH: templates/moko-cassiopeia/html/com_content/article/toc-right.php
* VERSION: 02.00
* BRIEF: Template override for Joomla articles with Table of Contents aligned right
* =========================================================================
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\Content\Administrator\Extension\ContentComponent;
use Joomla\Component\Content\Site\Helper\RouteHelper;
// Create shortcuts to some parameters.
$params = $this->item->params;
$canEdit = $params->get('access-edit');
$user = Factory::getUser();
$info = $params->get('info_block_position', 0);
$htag = $this->params->get('show_page_heading') ? 'h2' : 'h1';
// Check if associations are implemented. If they are, define the parameter.
$assocParam = (Associations::isEnabled() && $params->get('show_associations'));
$currentDate = Factory::getDate()->format('Y-m-d H:i:s');
$isNotPublishedYet = $this->item->publish_up > $currentDate;
$isExpired = !is_null($this->item->publish_down) && $this->item->publish_down < $currentDate;
?>
<div class="com-content-article item-page<?php echo $this->pageclass_sfx; ?>" itemscope itemtype="https://schema.org/Article">
<meta itemprop="inLanguage" content="<?php echo ($this->item->language === '*') ? Factory::getApplication()->get('language') : $this->item->language; ?>">
<?php if ($this->params->get('show_page_heading')) : ?>
<div class="page-header">
<h1> <?php echo $this->escape($this->params->get('page_heading')); ?> </h1>
</div>
<?php endif;
if (!empty($this->item->pagination) && !$this->item->paginationposition && $this->item->paginationrelative) {
echo $this->item->pagination;
}
?>
<?php $useDefList = $params->get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date')
|| $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam; ?>
<?php if ($params->get('show_title')) : ?>
<div class="page-header">
<<?php echo $htag; ?> itemprop="headline">
<?php echo $this->escape($this->item->title); ?>
</<?php echo $htag; ?>>
<?php if ($this->item->state == ContentComponent::CONDITION_UNPUBLISHED) : ?>
<span class="badge bg-warning text-light"><?php echo Text::_('JUNPUBLISHED'); ?></span>
<?php endif; ?>
<?php if ($isNotPublishedYet) : ?>
<span class="badge bg-warning text-light"><?php echo Text::_('JNOTPUBLISHEDYET'); ?></span>
<?php endif; ?>
<?php if ($isExpired) : ?>
<span class="badge bg-warning text-light"><?php echo Text::_('JEXPIRED'); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ($canEdit) : ?>
<?php echo LayoutHelper::render('joomla.content.icons', ['params' => $params, 'item' => $this->item]); ?>
<?php endif; ?>
<?php // Content is generated by content plugin event "onContentAfterTitle" ?>
<?php echo $this->item->event->afterDisplayTitle; ?>
<?php if ($useDefList && ($info == 0 || $info == 2)) : ?>
<?php echo LayoutHelper::render('joomla.content.info_block', ['item' => $this->item, 'params' => $params, 'position' => 'above']); ?>
<?php endif; ?>
<?php if ($info == 0 && $params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php $this->item->tagLayout = new FileLayout('joomla.content.tags'); ?>
<?php echo $this->item->tagLayout->render($this->item->tags->itemTags); ?>
<?php endif; ?>
<?php // Content is generated by content plugin event "onContentBeforeDisplay" ?>
<?php echo $this->item->event->beforeDisplayContent; ?>
<?php if ((int) $params->get('urls_position', 0) === 0) : ?>
<?php echo $this->loadTemplate('links'); ?>
<?php endif; ?>
<?php if ($params->get('access-view')) : ?>
<?php echo LayoutHelper::render('joomla.content.full_image', $this->item); ?>
<?php
if (!empty($this->item->pagination) && !$this->item->paginationposition && !$this->item->paginationrelative) :
echo $this->item->pagination;
endif;
?>
<div itemprop="articleBody" class="com-content-article__body">
<div class="container-toc-right">
<?php
// Table of Contents header using template language string
echo '<h2>' . Text::_('TPL_MOKO-CASSIOPEIA_TOC') . '</h2>';
?>
<nav id="toc" data-toggle="toc"></nav>
</div>
<?php
echo $this->item->text;
?>
</div>
<?php if ($info == 1 || $info == 2) : ?>
<?php if ($useDefList) : ?>
<?php echo LayoutHelper::render('joomla.content.info_block', ['item' => $this->item, 'params' => $params, 'position' => 'below']); ?>
<?php endif; ?>
<?php if ($params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php $this->item->tagLayout = new FileLayout('joomla.content.tags'); ?>
<?php echo $this->item->tagLayout->render($this->item->tags->itemTags); ?>
<?php endif; ?>
<?php endif; ?>
<?php
if (!empty($this->item->pagination) && $this->item->paginationposition && !$this->item->paginationrelative) :
echo $this->item->pagination;
?>
<?php endif; ?>
<?php if ((int) $params->get('urls_position', 0) === 1) : ?>
<?php echo $this->loadTemplate('links'); ?>
<?php endif; ?>
<?php // Optional teaser intro text for guests ?>
<?php elseif ($params->get('show_noauth') == true && $user->get('guest')) : ?>
<?php echo LayoutHelper::render('joomla.content.intro_image', $this->item); ?>
<?php echo HTMLHelper::_('content.prepare', $this->item->introtext); ?>
<?php // Optional link to let them register to see the whole article. ?>
<?php if ($params->get('show_readmore') && $this->item->fulltext != null) : ?>
<?php $menu = Factory::getApplication()->getMenu(); ?>
<?php $active = $menu->getActive(); ?>
<?php $itemId = $active->id; ?>
<?php $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); ?>
<?php $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); ?>
<?php echo LayoutHelper::render('joomla.content.readmore', ['item' => $this->item, 'params' => $params, 'link' => $link]); ?>
<?php endif; ?>
<?php endif; ?>
<?php
if (!empty($this->item->pagination) && $this->item->paginationposition && $this->item->paginationrelative) :
echo $this->item->pagination;
?>
<?php endif; ?>
<?php // Content is generated by content plugin event "onContentAfterDisplay" ?>
<?php echo $this->item->event->afterDisplayContent; ?>
</div>

View File

@@ -0,0 +1,33 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_content
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
// Add strings for translations in Javascript.
Text::script('JGLOBAL_EXPAND_CATEGORIES');
Text::script('JGLOBAL_COLLAPSE_CATEGORIES');
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $this->document->getWebAssetManager();
$wa->getRegistry()->addExtensionRegistryFile('com_categories');
$wa->usePreset('com_categories.shared-categories-accordion');
?>
<div class="com-content-categories categories-list">
<?php
echo LayoutHelper::render('joomla.content.categories_default', $this);
echo $this->loadTemplate('items');
?>
</div>

View File

@@ -0,0 +1,77 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_content
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\Component\Content\Site\Helper\RouteHelper;
if ($this->maxLevelcat != 0 && count($this->items[$this->parent->id]) > 0) :
?>
<div class="com-content-categories__items">
<?php foreach ($this->items[$this->parent->id] as $id => $item) : ?>
<?php if ($this->params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?>
<div class="com-content-categories__item">
<div class="com-content-categories__item-title-wrapper">
<div class="com-content-categories__item-title">
<a href="<?php echo Route::_(RouteHelper::getCategoryRoute($item->id, $item->language)); ?>">
<?php echo $this->escape($item->title); ?></a>
<?php if ($this->params->get('show_cat_num_articles_cat') == 1) :?>
<span class="badge bg-info">
<?php echo Text::_('COM_CONTENT_NUM_ITEMS'); ?>&nbsp;
<?php echo $item->numitems; ?>
</span>
<?php endif; ?>
</div>
<?php if (count($item->getChildren()) > 0 && $this->maxLevelcat > 1) : ?>
<button
type="button"
id="category-btn-<?php echo $item->id; ?>"
data-category-id="<?php echo $item->id; ?>"
class="btn btn-secondary btn-sm"
aria-expanded="false"
aria-label="<?php echo Text::_('JGLOBAL_EXPAND_CATEGORIES'); ?>"
>
<span class="icon-plus" aria-hidden="true"></span>
</button>
<?php endif; ?>
</div>
<?php if ($this->params->get('show_description_image') && $item->getParams()->get('image')) : ?>
<?php echo HTMLHelper::_('image', $item->getParams()->get('image'), $item->getParams()->get('image_alt')); ?>
<?php endif; ?>
<?php if ($this->params->get('show_subcat_desc_cat') == 1) : ?>
<?php if ($item->description) : ?>
<div class="com-content-categories__description category-desc">
<?php echo HTMLHelper::_('content.prepare', $item->description, '', 'com_content.categories'); ?>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if (count($item->getChildren()) > 0 && $this->maxLevelcat > 1) : ?>
<div class="com-content-categories__children" id="category-<?php echo $item->id; ?>" hidden="">
<?php
$this->items[$item->id] = $item->getChildren();
$this->parent = $item;
$this->maxLevelcat--;
echo $this->loadTemplate('items');
$this->parent = $item->getParent();
$this->maxLevelcat++;
?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>

View File

@@ -0,0 +1,118 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,143 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_content
*
* @copyright (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Layout\LayoutHelper;
$app = Factory::getApplication();
$this->category->text = $this->category->description;
$app->triggerEvent('onContentPrepare', [$this->category->extension . '.categories', &$this->category, &$this->params, 0]);
$this->category->description = $this->category->text;
$results = $app->triggerEvent('onContentAfterTitle', [$this->category->extension . '.categories', &$this->category, &$this->params, 0]);
$afterDisplayTitle = trim(implode("\n", $results));
$results = $app->triggerEvent('onContentBeforeDisplay', [$this->category->extension . '.categories', &$this->category, &$this->params, 0]);
$beforeDisplayContent = trim(implode("\n", $results));
$results = $app->triggerEvent('onContentAfterDisplay', [$this->category->extension . '.categories', &$this->category, &$this->params, 0]);
$afterDisplayContent = trim(implode("\n", $results));
$htag = $this->params->get('show_page_heading') ? 'h2' : 'h1';
?>
<div class="com-content-category-blog blog" itemscope itemtype="https://schema.org/Blog">
<?php if ($this->params->get('show_page_heading')) : ?>
<div class="page-header">
<h1> <?php echo $this->escape($this->params->get('page_heading')); ?> </h1>
</div>
<?php endif; ?>
<?php if ($this->params->get('show_category_title', 1)) : ?>
<<?php echo $htag; ?>>
<?php echo $this->category->title; ?>
</<?php echo $htag; ?>>
<?php endif; ?>
<?php echo $afterDisplayTitle; ?>
<?php if ($this->params->get('show_cat_tags', 1) && !empty($this->category->tags->itemTags)) : ?>
<?php $this->category->tagLayout = new FileLayout('joomla.content.tags'); ?>
<?php echo $this->category->tagLayout->render($this->category->tags->itemTags); ?>
<?php endif; ?>
<?php if ($beforeDisplayContent || $afterDisplayContent || $this->params->get('show_description', 1) || $this->params->def('show_description_image', 1)) : ?>
<div class="category-desc clearfix">
<?php if ($this->params->get('show_description_image') && $this->category->getParams()->get('image')) : ?>
<?php echo LayoutHelper::render(
'joomla.html.image',
[
'src' => $this->category->getParams()->get('image'),
'alt' => empty($this->category->getParams()->get('image_alt')) && empty($this->category->getParams()->get('image_alt_empty')) ? false : $this->category->getParams()->get('image_alt'),
]
); ?>
<?php endif; ?>
<?php echo $beforeDisplayContent; ?>
<?php if ($this->params->get('show_description') && $this->category->description) : ?>
<?php echo HTMLHelper::_('content.prepare', $this->category->description, '', 'com_content.category'); ?>
<?php endif; ?>
<?php echo $afterDisplayContent; ?>
</div>
<?php endif; ?>
<?php if (empty($this->lead_items) && empty($this->link_items) && empty($this->intro_items)) : ?>
<?php if ($this->params->get('show_no_articles', 1)) : ?>
<div class="alert alert-info">
<span class="icon-info-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('INFO'); ?></span>
<?php echo Text::_('COM_CONTENT_NO_ARTICLES'); ?>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if (!empty($this->lead_items)) : ?>
<div class="com-content-category-blog__items blog-items items-leading <?php echo $this->params->get('blog_class_leading'); ?>">
<?php foreach ($this->lead_items as &$item) : ?>
<div class="com-content-category-blog__item blog-item" itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
<?php
$this->item = &$item;
echo $this->loadTemplate('item');
?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($this->intro_items)) : ?>
<?php $blogClass = $this->params->get('blog_class', ''); ?>
<?php if ((int) $this->params->get('num_columns') > 1) : ?>
<?php $blogClass .= (int) $this->params->get('multi_column_order', 0) === 0 ? ' masonry-' : ' columns-'; ?>
<?php $blogClass .= (int) $this->params->get('num_columns'); ?>
<?php endif; ?>
<div class="com-content-category-blog__items blog-items <?php echo $blogClass; ?>">
<?php foreach ($this->intro_items as $key => &$item) : ?>
<div class="com-content-category-blog__item blog-item"
itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
<?php
$this->item = & $item;
echo $this->loadTemplate('item');
?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($this->link_items)) : ?>
<div class="items-more">
<?php echo $this->loadTemplate('links'); ?>
</div>
<?php endif; ?>
<?php if ($this->maxLevel != 0 && !empty($this->children[$this->category->id])) : ?>
<div class="com-content-category-blog__children cat-children">
<?php if ($this->params->get('show_category_heading_title_text', 1) == 1) : ?>
<h3> <?php echo Text::_('JGLOBAL_SUBCATEGORIES'); ?> </h3>
<?php endif; ?>
<?php echo $this->loadTemplate('children'); ?> </div>
<?php endif; ?>
<?php if (($this->params->def('show_pagination', 1) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?>
<div class="com-content-category-blog__navigation w-100">
<?php if ($this->params->def('show_pagination_results', 1)) : ?>
<p class="com-content-category-blog__counter counter float-end pt-3 pe-2">
<?php echo $this->pagination->getPagesCounter(); ?>
</p>
<?php endif; ?>
<div class="com-content-category-blog__pagination">
<?php echo $this->pagination->getPagesLinks(); ?>
</div>
</div>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,86 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_content
*
* @copyright (C) 2010 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\Component\Content\Site\Helper\RouteHelper;
$lang = Factory::getLanguage();
$user = Factory::getUser();
$groups = $user->getAuthorisedViewLevels();
if ($this->maxLevel != 0 && count($this->children[$this->category->id]) > 0) : ?>
<?php foreach ($this->children[$this->category->id] as $id => $child) : ?>
<?php // Check whether category access level allows access to subcategories. ?>
<?php if (in_array($child->access, $groups)) : ?>
<?php if ($this->params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?>
<div class="com-content-category-blog__child">
<?php if ($lang->isRtl()) : ?>
<h3 class="page-header item-title">
<?php if ($this->params->get('show_cat_num_articles', 1)) : ?>
<span class="badge bg-info tip">
<?php echo $child->getNumItems(true); ?>
</span>
<?php endif; ?>
<a href="<?php echo Route::_(RouteHelper::getCategoryRoute($child->id, $child->language)); ?>">
<?php echo $this->escape($child->title); ?></a>
<?php if ($this->maxLevel > 1 && count($child->getChildren()) > 0) : ?>
<a href="#category-<?php echo $child->id; ?>" data-bs-toggle="collapse" class="btn btn-sm float-end" aria-label="<?php echo Text::_('JGLOBAL_EXPAND_CATEGORIES'); ?>"><span class="icon-plus" aria-hidden="true"></span></a>
<?php endif; ?>
</h3>
<?php else : ?>
<h3 class="page-header item-title"><a href="<?php echo Route::_(RouteHelper::getCategoryRoute($child->id, $child->language)); ?>">
<?php echo $this->escape($child->title); ?></a>
<?php if ($this->params->get('show_cat_num_articles', 1)) : ?>
<span class="badge bg-info">
<?php echo Text::_('COM_CONTENT_NUM_ITEMS'); ?>&nbsp;
<?php echo $child->getNumItems(true); ?>
</span>
<?php endif; ?>
<?php if ($this->maxLevel > 1 && count($child->getChildren()) > 0) : ?>
<a href="#category-<?php echo $child->id; ?>" data-bs-toggle="collapse" class="btn btn-sm float-end" aria-label="<?php echo Text::_('JGLOBAL_EXPAND_CATEGORIES'); ?>"><span class="icon-plus" aria-hidden="true"></span></a>
<?php endif; ?>
</h3>
<?php endif; ?>
<?php if ($this->params->get('show_subcat_desc') == 1) : ?>
<?php if ($child->description) : ?>
<div class="com-content-category-blog__description category-desc">
<?php echo HTMLHelper::_('content.prepare', $child->description, '', 'com_content.category'); ?>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if ($this->maxLevel > 1 && count($child->getChildren()) > 0) : ?>
<div class="com-content-category-blog__children collapse fade" id="category-<?php echo $child->id; ?>">
<?php
$this->children[$child->id] = $child->getChildren();
$this->category = $child;
$this->maxLevel--;
echo $this->loadTemplate('children');
$this->category = $child->getParent();
$this->maxLevel++;
?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
<?php endforeach; ?>
<?php endif;

View File

@@ -0,0 +1,102 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_content
*
* @copyright (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Associations;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\Content\Administrator\Extension\ContentComponent;
use Joomla\Component\Content\Site\Helper\RouteHelper;
// Create a shortcut for params.
$params = $this->item->params;
$canEdit = $this->item->params->get('access-edit');
$info = $params->get('info_block_position', 0);
// Check if associations are implemented. If they are, define the parameter.
$assocParam = (Associations::isEnabled() && $params->get('show_associations'));
$currentDate = Factory::getDate()->format('Y-m-d H:i:s');
$isUnpublished = ($this->item->state == ContentComponent::CONDITION_UNPUBLISHED || $this->item->publish_up > $currentDate)
|| ($this->item->publish_down < $currentDate && $this->item->publish_down !== null);
?>
<section id="<?php echo $this->item->alias;?>">
<?php echo LayoutHelper::render('joomla.content.intro_image', $this->item); ?>
<div class="item-content">
<?php if ($isUnpublished) : ?>
<div class="system-unpublished">
<?php endif; ?>
<?php echo LayoutHelper::render('joomla.content.blog_style_default_item_title', $this->item); ?>
<?php if ($canEdit) : ?>
<?php echo LayoutHelper::render('joomla.content.icons', ['params' => $params, 'item' => $this->item]); ?>
<?php endif; ?>
<?php // @todo Not that elegant would be nice to group the params ?>
<?php $useDefList = ($params->get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date')
|| $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?>
<?php if ($useDefList && ($info == 0 || $info == 2)) : ?>
<?php echo LayoutHelper::render('joomla.content.info_block', ['item' => $this->item, 'params' => $params, 'position' => 'above']); ?>
<?php endif; ?>
<?php if ($info == 0 && $params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?>
<?php if (!$params->get('show_intro')) : ?>
<?php // Content is generated by content plugin event "onContentAfterTitle" ?>
<?php echo $this->item->event->afterDisplayTitle; ?>
<?php endif; ?>
<?php // Content is generated by content plugin event "onContentBeforeDisplay" ?>
<?php echo $this->item->event->beforeDisplayContent; ?>
<?php echo $this->item->introtext; ?>
<?php if ($info == 1 || $info == 2) : ?>
<?php if ($useDefList) : ?>
<?php echo LayoutHelper::render('joomla.content.info_block', ['item' => $this->item, 'params' => $params, 'position' => 'below']); ?>
<?php endif; ?>
<?php if ($params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?>
<?php endif; ?>
<?php if ($params->get('show_readmore') && $this->item->readmore) :
if ($params->get('access-view')) :
$link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language));
else :
$menu = Factory::getApplication()->getMenu();
$active = $menu->getActive();
$itemId = $active->id;
$link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false));
$link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)));
endif; ?>
<?php echo LayoutHelper::render('joomla.content.readmore', ['item' => $this->item, 'params' => $params, 'link' => $link]); ?>
<?php endif; ?>
<?php if ($isUnpublished) : ?>
</div>
<?php endif; ?>
<?php // Content is generated by content plugin event "onContentAfterDisplay" ?>
<?php echo $this->item->event->afterDisplayContent; ?>
</div>
</section>

View File

@@ -0,0 +1,27 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_content
*
* @copyright (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\Component\Content\Site\Helper\RouteHelper;
?>
<ol class="com-content-blog__links">
<?php foreach ($this->link_items as $item) : ?>
<li class="com-content-blog__link">
<a href="<?php echo Route::_(RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language)); ?>">
<?php echo $item->title; ?></a>
</li>
<?php endforeach; ?>
</ol>

View File

@@ -0,0 +1,25 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_content
*
* @copyright (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Layout\LayoutHelper;
?>
<div class="com-content-category category-list">
<?php
$this->subtemplatename = 'articles';
echo LayoutHelper::render('joomla.content.category_default', $this);
?>
</div>

View File

@@ -0,0 +1,349 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_content
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\Content\Administrator\Extension\ContentComponent;
use Joomla\Component\Content\Site\Helper\AssociationHelper;
use Joomla\Component\Content\Site\Helper\RouteHelper;
/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
$wa = $this->document->getWebAssetManager();
$wa->useScript('com_content.articles-list');
// Create some shortcuts.
$n = count($this->items);
$listOrder = $this->escape($this->state->get('list.ordering'));
$listDirn = $this->escape($this->state->get('list.direction'));
$langFilter = false;
// Tags filtering based on language filter
if (($this->params->get('filter_field') === 'tag') && (Multilanguage::isEnabled())) {
$tagfilter = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter');
switch ($tagfilter) {
case 'current_language':
$langFilter = Factory::getApplication()->getLanguage()->getTag();
break;
case 'all':
$langFilter = false;
break;
default:
$langFilter = $tagfilter;
}
}
// Check for at least one editable article
$isEditable = false;
if (!empty($this->items)) {
foreach ($this->items as $article) {
if ($article->params->get('access-edit')) {
$isEditable = true;
break;
}
}
}
$currentDate = Factory::getDate()->format('Y-m-d H:i:s');
?>
<form action="<?php echo htmlspecialchars(Uri::getInstance()->toString()); ?>" method="post" name="adminForm" id="adminForm" class="com-content-category__articles">
<?php if ($this->params->get('filter_field') !== 'hide') : ?>
<div class="com-content__filter btn-group">
<?php if ($this->params->get('filter_field') === 'tag') : ?>
<span class="visually-hidden">
<label class="filter-search-lbl" for="filter-search">
<?php echo Text::_('JOPTION_SELECT_TAG'); ?>
</label>
</span>
<select name="filter_tag" id="filter-search" class="form-select" onchange="document.adminForm.submit();" >
<option value=""><?php echo Text::_('JOPTION_SELECT_TAG'); ?></option>
<?php echo HTMLHelper::_('select.options', HTMLHelper::_('tag.options', ['filter.published' => [1], 'filter.language' => $langFilter], true), 'value', 'text', $this->state->get('filter.tag')); ?>
</select>
<?php elseif ($this->params->get('filter_field') === 'month') : ?>
<span class="visually-hidden">
<label class="filter-search-lbl" for="filter-search">
<?php echo Text::_('JOPTION_SELECT_MONTH'); ?>
</label>
</span>
<select name="filter-search" id="filter-search" class="form-select" onchange="document.adminForm.submit();">
<option value=""><?php echo Text::_('JOPTION_SELECT_MONTH'); ?></option>
<?php echo HTMLHelper::_('select.options', HTMLHelper::_('content.months', $this->state), 'value', 'text', $this->state->get('list.filter')); ?>
</select>
<?php else : ?>
<label class="filter-search-lbl visually-hidden" for="filter-search">
<?php echo Text::_('COM_CONTENT_' . $this->params->get('filter_field') . '_FILTER_LABEL'); ?>
</label>
<input type="text" name="filter-search" id="filter-search" value="<?php echo $this->escape($this->state->get('list.filter')); ?>" class="inputbox" onchange="document.adminForm.submit();" placeholder="<?php echo Text::_('COM_CONTENT_' . $this->params->get('filter_field') . '_FILTER_LABEL'); ?>">
<?php endif; ?>
<?php if ($this->params->get('filter_field') !== 'tag' && $this->params->get('filter_field') !== 'month') : ?>
<button type="submit" name="filter_submit" class="btn btn-primary"><?php echo Text::_('JGLOBAL_FILTER_BUTTON'); ?></button>
<?php endif; ?>
<button type="reset" name="filter-clear-button" class="btn btn-secondary"><?php echo Text::_('JSEARCH_FILTER_CLEAR'); ?></button>
</div>
<?php endif; ?>
<?php if ($this->params->get('show_pagination_limit')) : ?>
<div class="com-content-category__pagination btn-group float-end">
<label for="limit" class="visually-hidden">
<?php echo Text::_('JGLOBAL_DISPLAY_NUM'); ?>
</label>
<?php echo $this->pagination->getLimitBox(); ?>
</div>
<?php endif; ?>
<?php if (empty($this->items)) : ?>
<?php if ($this->params->get('show_no_articles', 1)) : ?>
<div class="alert alert-info">
<span class="icon-info-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('INFO'); ?></span>
<?php echo Text::_('COM_CONTENT_NO_ARTICLES'); ?>
</div>
<?php endif; ?>
<?php else : ?>
<table class="com-content-category__table category table table-striped table-bordered table-hover">
<caption class="visually-hidden">
<?php echo Text::_('COM_CONTENT_ARTICLES_TABLE_CAPTION'); ?>
</caption>
<thead<?php echo $this->params->get('show_headings', '1') ? '' : ' class="visually-hidden"'; ?>>
<tr>
<th scope="col" id="categorylist_header_title">
<?php echo HTMLHelper::_('grid.sort', 'JGLOBAL_TITLE', 'a.title', $listDirn, $listOrder, null, 'asc', '', 'adminForm'); ?>
</th>
<?php if ($date = $this->params->get('list_show_date')) : ?>
<th scope="col" id="categorylist_header_date">
<?php if ($date === 'created') : ?>
<?php echo HTMLHelper::_('grid.sort', 'COM_CONTENT_' . $date . '_DATE', 'a.created', $listDirn, $listOrder); ?>
<?php elseif ($date === 'modified') : ?>
<?php echo HTMLHelper::_('grid.sort', 'COM_CONTENT_' . $date . '_DATE', 'a.modified', $listDirn, $listOrder); ?>
<?php elseif ($date === 'published') : ?>
<?php echo HTMLHelper::_('grid.sort', 'COM_CONTENT_' . $date . '_DATE', 'a.publish_up', $listDirn, $listOrder); ?>
<?php endif; ?>
</th>
<?php endif; ?>
<?php if ($this->params->get('list_show_author')) : ?>
<th scope="col" id="categorylist_header_author">
<?php echo HTMLHelper::_('grid.sort', 'JAUTHOR', 'author', $listDirn, $listOrder); ?>
</th>
<?php endif; ?>
<?php if ($this->params->get('list_show_hits')) : ?>
<th scope="col" id="categorylist_header_hits">
<?php echo HTMLHelper::_('grid.sort', 'JGLOBAL_HITS', 'a.hits', $listDirn, $listOrder); ?>
</th>
<?php endif; ?>
<?php if ($this->params->get('list_show_votes', 0) && $this->vote) : ?>
<th scope="col" id="categorylist_header_votes">
<?php echo HTMLHelper::_('grid.sort', 'COM_CONTENT_VOTES', 'rating_count', $listDirn, $listOrder); ?>
</th>
<?php endif; ?>
<?php if ($this->params->get('list_show_ratings', 0) && $this->vote) : ?>
<th scope="col" id="categorylist_header_ratings">
<?php echo HTMLHelper::_('grid.sort', 'COM_CONTENT_RATINGS', 'rating', $listDirn, $listOrder); ?>
</th>
<?php endif; ?>
<?php if ($isEditable) : ?>
<th scope="col" id="categorylist_header_edit"><?php echo Text::_('COM_CONTENT_EDIT_ITEM'); ?></th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($this->items as $i => $article) : ?>
<?php if ($this->items[$i]->state == ContentComponent::CONDITION_UNPUBLISHED) : ?>
<tr class="system-unpublished cat-list-row<?php echo $i % 2; ?>">
<?php else : ?>
<tr class="cat-list-row<?php echo $i % 2; ?>" >
<?php endif; ?>
<th class="list-title" scope="row">
<?php if (in_array($article->access, $this->user->getAuthorisedViewLevels())) : ?>
<a href="<?php echo Route::_(RouteHelper::getArticleRoute($article->slug, $article->catid, $article->language)); ?>">
<?php echo $this->escape($article->title); ?>
</a>
<?php if (Associations::isEnabled() && $this->params->get('show_associations')) : ?>
<div class="cat-list-association">
<?php $associations = AssociationHelper::displayAssociations($article->id); ?>
<?php foreach ($associations as $association) : ?>
<?php if ($this->params->get('flags', 1) && $association['language']->image) : ?>
<?php $flag = HTMLHelper::_('image', 'mod_languages/' . $association['language']->image . '.gif', $association['language']->title_native, ['title' => $association['language']->title_native], true); ?>
<a href="<?php echo Route::_($association['item']); ?>"><?php echo $flag; ?></a>
<?php else : ?>
<?php $class = 'btn btn-secondary btn-sm btn-' . strtolower($association['language']->lang_code); ?>
<a class="<?php echo $class; ?>" title="<?php echo $association['language']->title_native; ?>" href="<?php echo Route::_($association['item']); ?>"><?php echo $association['language']->lang_code; ?>
<span class="visually-hidden"><?php echo $association['language']->title_native; ?></span>
</a>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php else : ?>
<?php
echo $this->escape($article->title) . ' : ';
$itemId = Factory::getApplication()->getMenu()->getActive()->id;
$link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false));
$link->setVar('return', base64_encode(RouteHelper::getArticleRoute($article->slug, $article->catid, $article->language)));
?>
<a href="<?php echo $link; ?>" class="register">
<?php echo Text::_('COM_CONTENT_REGISTER_TO_READ_MORE'); ?>
</a>
<?php if (Associations::isEnabled() && $this->params->get('show_associations')) : ?>
<div class="cat-list-association">
<?php $associations = AssociationHelper::displayAssociations($article->id); ?>
<?php foreach ($associations as $association) : ?>
<?php if ($this->params->get('flags', 1)) : ?>
<?php $flag = HTMLHelper::_('image', 'mod_languages/' . $association['language']->image . '.gif', $association['language']->title_native, ['title' => $association['language']->title_native], true); ?>
<a href="<?php echo Route::_($association['item']); ?>"><?php echo $flag; ?></a>
<?php else : ?>
<?php $class = 'btn btn-secondary btn-sm btn-' . strtolower($association['language']->lang_code); ?>
<a class="<?php echo $class; ?>" title="<?php echo $association['language']->title_native; ?>" href="<?php echo Route::_($association['item']); ?>"><?php echo $association['language']->lang_code; ?>
<span class="visually-hidden"><?php echo $association['language']->title_native; ?></span>
</a>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if ($article->state == ContentComponent::CONDITION_UNPUBLISHED) : ?>
<div>
<span class="list-published badge bg-warning text-light">
<?php echo Text::_('JUNPUBLISHED'); ?>
</span>
</div>
<?php endif; ?>
<?php if ($article->publish_up > $currentDate) : ?>
<div>
<span class="list-published badge bg-warning text-light">
<?php echo Text::_('JNOTPUBLISHEDYET'); ?>
</span>
</div>
<?php endif; ?>
<?php if (!is_null($article->publish_down) && $article->publish_down < $currentDate) : ?>
<div>
<span class="list-published badge bg-warning text-light">
<?php echo Text::_('JEXPIRED'); ?>
</span>
</div>
<?php endif; ?>
</th>
<?php if ($this->params->get('list_show_date')) : ?>
<td class="list-date small">
<?php
echo HTMLHelper::_(
'date',
$article->displayDate,
$this->escape($this->params->get('date_format', Text::_('DATE_FORMAT_LC3')))
); ?>
</td>
<?php endif; ?>
<?php if ($this->params->get('list_show_author', 1)) : ?>
<td class="list-author">
<?php if (!empty($article->author) || !empty($article->created_by_alias)) : ?>
<?php $author = $article->author ?>
<?php $author = $article->created_by_alias ?: $author; ?>
<?php if (!empty($article->contact_link) && $this->params->get('link_author') == true) : ?>
<?php if ($this->params->get('show_headings')) : ?>
<?php echo HTMLHelper::_('link', $article->contact_link, $author); ?>
<?php else : ?>
<?php echo Text::sprintf('COM_CONTENT_WRITTEN_BY', HTMLHelper::_('link', $article->contact_link, $author)); ?>
<?php endif; ?>
<?php else : ?>
<?php if ($this->params->get('show_headings')) : ?>
<?php echo $author; ?>
<?php else : ?>
<?php echo Text::sprintf('COM_CONTENT_WRITTEN_BY', $author); ?>
<?php endif; ?>
<?php endif; ?>
<?php endif; ?>
</td>
<?php endif; ?>
<?php if ($this->params->get('list_show_hits', 1)) : ?>
<td class="list-hits">
<span class="badge bg-info">
<?php if ($this->params->get('show_headings')) : ?>
<?php echo $article->hits; ?>
<?php else : ?>
<?php echo Text::sprintf('JGLOBAL_HITS_COUNT', $article->hits); ?>
<?php endif; ?>
</span>
</td>
<?php endif; ?>
<?php if ($this->params->get('list_show_votes', 0) && $this->vote) : ?>
<td class="list-votes">
<span class="badge bg-success">
<?php if ($this->params->get('show_headings')) : ?>
<?php echo $article->rating_count; ?>
<?php else : ?>
<?php echo Text::sprintf('COM_CONTENT_VOTES_COUNT', $article->rating_count); ?>
<?php endif; ?>
</span>
</td>
<?php endif; ?>
<?php if ($this->params->get('list_show_ratings', 0) && $this->vote) : ?>
<td class="list-ratings">
<span class="badge bg-warning text-light">
<?php if ($this->params->get('show_headings')) : ?>
<?php echo $article->rating; ?>
<?php else : ?>
<?php echo Text::sprintf('COM_CONTENT_RATINGS_COUNT', $article->rating); ?>
<?php endif; ?>
</span>
</td>
<?php endif; ?>
<?php if ($isEditable) : ?>
<td class="list-edit">
<?php if ($article->params->get('access-edit')) : ?>
<?php echo HTMLHelper::_('contenticon.edit', $article, $article->params); ?>
<?php endif; ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php // Code to add a link to submit an article. ?>
<?php if ($this->category->getParams()->get('access-create')) : ?>
<?php echo HTMLHelper::_('contenticon.create', $this->category, $this->category->params); ?>
<?php endif; ?>
<?php // Add pagination links ?>
<?php if (!empty($this->items)) : ?>
<?php if (($this->params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?>
<div class="com-content-category__navigation w-100">
<?php if ($this->params->def('show_pagination_results', 1)) : ?>
<p class="com-content-category__counter counter float-end pt-3 pe-2">
<?php echo $this->pagination->getPagesCounter(); ?>
</p>
<?php endif; ?>
<div class="com-content-category__pagination">
<?php echo $this->pagination->getPagesLinks(); ?>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
<div>
<input type="hidden" name="filter_order" value="">
<input type="hidden" name="filter_order_Dir" value="">
<input type="hidden" name="limitstart" value="">
<input type="hidden" name="task" value="">
</div>
</form>

View File

@@ -0,0 +1,85 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_content
*
* @copyright (C) 2009 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\Component\Content\Site\Helper\RouteHelper;
$lang = Factory::getLanguage();
$user = Factory::getUser();
$groups = $user->getAuthorisedViewLevels();
?>
<?php if (count($this->children[$this->category->id]) > 0) : ?>
<?php foreach ($this->children[$this->category->id] as $id => $child) : ?>
<?php // Check whether category access level allows access to subcategories. ?>
<?php if (in_array($child->access, $groups)) : ?>
<?php if ($this->params->get('show_empty_categories') || $child->getNumItems(true) || count($child->getChildren())) : ?>
<div class="com-content-category__children">
<?php if ($lang->isRtl()) : ?>
<h3 class="page-header item-title">
<?php if ($this->params->get('show_cat_num_articles', 1)) : ?>
<span class="badge bg-info tip hasTooltip" title="<?php echo HTMLHelper::_('tooltipText', 'COM_CONTENT_NUM_ITEMS'); ?>">
<?php echo $child->getNumItems(true); ?>
</span>
<?php endif; ?>
<a href="<?php echo Route::_(RouteHelper::getCategoryRoute($child->id, $child->language)); ?>">
<?php echo $this->escape($child->title); ?></a>
<?php if (count($child->getChildren()) > 0 && $this->maxLevel > 1) : ?>
<a href="#category-<?php echo $child->id; ?>" data-bs-toggle="collapse" class="btn btn-sm float-end" aria-label="<?php echo Text::_('JGLOBAL_EXPAND_CATEGORIES'); ?>"><span class="icon-plus" aria-hidden="true"></span></a>
<?php endif; ?>
</h3>
<?php else : ?>
<h3 class="page-header item-title"><a href="<?php echo Route::_(RouteHelper::getCategoryRoute($child->id, $child->language)); ?>">
<?php echo $this->escape($child->title); ?></a>
<?php if ($this->params->get('show_cat_num_articles', 1)) : ?>
<span class="badge bg-info tip hasTooltip" title="<?php echo HTMLHelper::_('tooltipText', 'COM_CONTENT_NUM_ITEMS'); ?>">
<?php echo $child->getNumItems(true); ?>
</span>
<?php endif; ?>
<?php if (count($child->getChildren()) > 0 && $this->maxLevel > 1) : ?>
<a href="#category-<?php echo $child->id; ?>" data-bs-toggle="collapse" class="btn btn-sm float-end" aria-label="<?php echo Text::_('JGLOBAL_EXPAND_CATEGORIES'); ?>"><span class="icon-plus" aria-hidden="true"></span></a>
<?php endif; ?>
</h3>
<?php endif; ?>
<?php if ($this->params->get('show_subcat_desc') == 1) : ?>
<?php if ($child->description) : ?>
<div class="category-desc">
<?php echo HTMLHelper::_('content.prepare', $child->description, '', 'com_content.category'); ?>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if (count($child->getChildren()) > 0 && $this->maxLevel > 1) : ?>
<div class="collapse fade" id="category-<?php echo $child->id; ?>">
<?php
$this->children[$child->id] = $child->getChildren();
$this->category = $child;
$this->maxLevel--;
echo $this->loadTemplate('children');
$this->category = $child->getParent();
$this->maxLevel++;
?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>

View File

@@ -0,0 +1,118 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,75 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_content
*
* @copyright (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
?>
<div class="blog-featured" itemscope itemtype="https://schema.org/Blog">
<?php if ($this->params->get('show_page_heading') != 0) : ?>
<div class="page-header">
<h1>
<?php echo $this->escape($this->params->get('page_heading')); ?>
</h1>
</div>
<?php endif; ?>
<?php if (!empty($this->lead_items)) : ?>
<div class="blog-items items-leading <?php echo $this->params->get('blog_class_leading'); ?>">
<?php foreach ($this->lead_items as &$item) : ?>
<div class="blog-item"
itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
<?php
$this->item = & $item;
echo $this->loadTemplate('item');
?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($this->intro_items)) : ?>
<?php $blogClass = $this->params->get('blog_class', ''); ?>
<?php if ((int) $this->params->get('num_columns') > 1) : ?>
<?php $blogClass .= (int) $this->params->get('multi_column_order', 0) === 0 ? ' masonry-' : ' columns-'; ?>
<?php $blogClass .= (int) $this->params->get('num_columns'); ?>
<?php endif; ?>
<div class="blog-items <?php echo $blogClass; ?>">
<?php foreach ($this->intro_items as $key => &$item) : ?>
<div class="blog-item"
itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
<?php
$this->item = & $item;
echo $this->loadTemplate('item');
?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($this->link_items)) : ?>
<div class="items-more">
<?php echo $this->loadTemplate('links'); ?>
</div>
<?php endif; ?>
<?php if ($this->params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2 && $this->pagination->pagesTotal > 1)) : ?>
<div class="w-100">
<?php if ($this->params->def('show_pagination_results', 1)) : ?>
<p class="counter float-end pt-3 pe-2">
<?php echo $this->pagination->getPagesCounter(); ?>
</p>
<?php endif; ?>
<?php echo $this->pagination->getPagesLinks(); ?>
</div>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,121 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_content
*
* @copyright (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Associations;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\Content\Administrator\Extension\ContentComponent;
use Joomla\Component\Content\Site\Helper\RouteHelper;
// Create a shortcut for params.
$params = &$this->item->params;
$canEdit = $this->item->params->get('access-edit');
$info = $this->item->params->get('info_block_position', 0);
// Check if associations are implemented. If they are, define the parameter.
$assocParam = (Associations::isEnabled() && $params->get('show_associations'));
$currentDate = Factory::getDate()->format('Y-m-d H:i:s');
$isExpired = !is_null($this->item->publish_down) && $this->item->publish_down < $currentDate;
$isNotPublishedYet = $this->item->publish_up > $currentDate;
$isUnpublished = $this->item->state == ContentComponent::CONDITION_UNPUBLISHED || $isNotPublishedYet || $isExpired;
?>
<?php echo LayoutHelper::render('joomla.content.intro_image', $this->item); ?>
<div class="item-content">
<?php if ($isUnpublished) : ?>
<div class="system-unpublished">
<?php endif; ?>
<?php if ($params->get('show_title')) : ?>
<h2 class="item-title" itemprop="headline">
<?php if ($params->get('link_titles') && $params->get('access-view')) : ?>
<a href="<?php echo Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)); ?>" itemprop="url">
<?php echo $this->escape($this->item->title); ?>
</a>
<?php else : ?>
<?php echo $this->escape($this->item->title); ?>
<?php endif; ?>
</h2>
<?php endif; ?>
<?php if ($this->item->state == ContentComponent::CONDITION_UNPUBLISHED) : ?>
<span class="badge bg-warning text-light"><?php echo Text::_('JUNPUBLISHED'); ?></span>
<?php endif; ?>
<?php if ($isNotPublishedYet) : ?>
<span class="badge bg-warning text-light"><?php echo Text::_('JNOTPUBLISHEDYET'); ?></span>
<?php endif; ?>
<?php if ($isExpired) : ?>
<span class="badge bg-warning text-light"><?php echo Text::_('JEXPIRED'); ?></span>
<?php endif; ?>
<?php if ($canEdit) : ?>
<?php echo LayoutHelper::render('joomla.content.icons', ['params' => $params, 'item' => $this->item]); ?>
<?php endif; ?>
<?php // Content is generated by content plugin event "onContentAfterTitle" ?>
<?php echo $this->item->event->afterDisplayTitle; ?>
<?php // @todo Not that elegant would be nice to group the params ?>
<?php $useDefList = ($params->get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date')
|| $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?>
<?php if ($useDefList && ($info == 0 || $info == 2)) : ?>
<?php echo LayoutHelper::render('joomla.content.info_block', ['item' => $this->item, 'params' => $params, 'position' => 'above']); ?>
<?php endif; ?>
<?php if ($info == 0 && $params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?>
<?php // Content is generated by content plugin event "onContentBeforeDisplay" ?>
<?php echo $this->item->event->beforeDisplayContent; ?>
<?php echo $this->item->introtext; ?>
<?php if ($info == 1 || $info == 2) : ?>
<?php if ($useDefList) : ?>
<?php echo LayoutHelper::render('joomla.content.info_block', ['item' => $this->item, 'params' => $params, 'position' => 'below']); ?>
<?php endif; ?>
<?php if ($params->get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?>
<?php echo LayoutHelper::render('joomla.content.tags', $this->item->tags->itemTags); ?>
<?php endif; ?>
<?php endif; ?>
<?php if ($params->get('show_readmore') && $this->item->readmore) :
if ($params->get('access-view')) :
$link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language));
else :
$menu = Factory::getApplication()->getMenu();
$active = $menu->getActive();
$itemId = $active->id;
$link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false));
$link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)));
endif; ?>
<?php echo LayoutHelper::render('joomla.content.readmore', ['item' => $this->item, 'params' => $params, 'link' => $link]); ?>
<?php endif; ?>
<?php if ($isUnpublished) : ?>
</div>
<?php endif; ?>
</div>
<?php // Content is generated by content plugin event "onContentAfterDisplay" ?>
<?php echo $this->item->event->afterDisplayContent; ?>

View File

@@ -0,0 +1,26 @@
<?php
/**
* @package Joomla.Site
* @subpackage com_content
*
* @copyright (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
defined('_JEXEC') or die;
use Joomla\CMS\Router\Route;
use Joomla\Component\Content\Site\Helper\RouteHelper;
?>
<ol class="com-content-blog__links">
<?php foreach ($this->link_items as $item) : ?>
<li class="com-content-blog__link">
<a href="<?php echo Route::_(RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language)); ?>">
<?php echo $item->title; ?></a>
</li>
<?php endforeach; ?>
</ol>

View File

@@ -0,0 +1,118 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,118 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,78 @@
<?php
/**
* @package AkeebaEngage
* @copyright Copyright (c)2020-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') or die();
/**
* View Template for comments display
*
* This is the main view template used when comments are being displayed e.g. at the end of an article.
*
* This provides the outer HTML structure of the comments.
*
* It loads the following view templates:
* - default_list.php The threaded list of comments
* - default_login.php Login form for guest users
* - default_form.php Comment / reply submission form
*/
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Language\Text;
/** @var \Akeeba\Component\Engage\Site\View\Comments\HtmlView $this */
$cParams = ComponentHelper::getParams('com_engage');
?>
<section id="akengage-comments-section" class="akengage-outer-container"
aria-label="<?= Text::_('COM_ENGAGE_COMMENTS_SECTION_HEADER') ?>">
<h3 class="akengage-title h4 border-bottom mb-2" data-toc-skip>
<?= Text::plural($this->headerKey, $this->pagination->total, $this->title) ?>
</h3>
<?= $this->loadPosition('engage-before-comments') ?>
<?php if ($this->pagination->total): ?>
<div class="akengage-list-container">
<?= $this->loadTemplate('list') ?>
</div>
<?= $this->loadPosition('engage-after-comments') ?>
<?php if ($this->pagination->pagesTotal > 1): ?>
<div class="akengage-pagination">
<div class="akengage-pagination-pages pagination" itemscope itemtype="http://www.schema.org/SiteNavigationElement">
<?= $this->pagination->getPagesLinks() ?>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if (!$this->areCommentsClosed && $this->user->guest && !$this->perms['create']): ?>
<?= $this->loadTemplate('login') ?>
<?php endif; ?>
<?php if ($this->perms['create'] && !$this->areCommentsClosed): ?>
<?= $this->loadTemplate('form') ?>
<?php endif; ?>
<?php if ($this->perms['create'] && $this->areCommentsClosed): ?>
<div class="alert alert-info">
<h3 class="alert-heading">
<?= Text::_('COM_ENGAGE_COMMENTS_LBL_CLOSED_HEADER') ?>
</h3>
<p>
<?php if ($this->areCommentsClosedAfterTime): ?>
<?= Text::_('COM_ENGAGE_COMMENTS_LBL_CLOSED_AFTERTIME') ?>
<?php else: ?>
<?= Text::_('COM_ENGAGE_COMMENTS_LBL_CLOSED_BODY') ?>
<?php endif; ?>
</p>
</div>
<?php endif; ?>
</section>

View File

@@ -0,0 +1,90 @@
<?php
/**
* @package AkeebaEngage
* @copyright Copyright (c)2020-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') or die();
/**
* View Template for the submitting comments.
*
* This is called by default.php
*/
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
/** @var \Akeeba\Component\Engage\Site\View\Comments\HtmlView $this */
$cParams = ComponentHelper::getParams('com_engage');
$badUx = ($cParams->get('comments_reply_bad_ux', 0) == 1) && empty($this->form->getValue('body'));
HTMLHelper::_('behavior.formvalidator');
?>
<?php if ($badUx): ?>
<div class="akengage-comment-hider" id="akengage-comment-hider">
<button type="button"
id="akengage-comment-hider-button"
class="btn btn-primary">
<?= Text::_('COM_ENGAGE_COMMENTS_FORM_HEADER'); ?>
</button>
</div>
<?php endif; ?>
<form action="<?= Route::_('index.php?option=com_engage&task=comment.save') ?>"
method="post" name="akengage-comment-form" id="akengageCommentForm"
class="form-validate <?= $badUx ? 'd-none' : ''; ?>"
style="<?= $badUx ? 'display: none;' : ''; ?>"
aria-label="<?= Text::_('COM_ENGAGE_COMMENTS_FORM_HEADER', true) ?>"
>
<input type="hidden" name="returnurl" value="<?= base64_encode(Uri::getInstance()->toString(['scheme', 'user', 'pass', 'host', 'port', 'path', 'query', 'fragment'])) ?>">
<input type="hidden" name="view" value="">
<input type="hidden" name="id" value="">
<?= HTMLHelper::_('form.token') ?>
<div class="mt-3 pt-2 mb-2 border-top border-2 border-dark">
<h3 class="h1 my-3">
<?= Text::_('COM_ENGAGE_COMMENTS_FORM_HEADER') ?>
</h3>
<?= $this->loadPosition('engage-before-reply'); ?>
<div id="akengage-comment-inreplyto-wrapper" class="alert alert-info d-none">
<div class="d-flex flex-wrap">
<div class="flex-grow-1">
<?= Text::_('COM_ENGAGE_COMMENTS_FORM_INREPLYTO_LABEL'); ?>
<span id="akengage-comment-inreplyto-name" class="text-secondary fw-bold">Some User</span>
</div>
<button id="akengage-comment-inreplyto-cancel"
type="button"
class="ms-2 btn btn-sm btn-outline-danger"
><?= Text::_('COM_ENGAGE_COMMENTS_FORM_CANCELREPLY'); ?></button>
</div>
</div>
<?php foreach (array_keys($this->form->getFieldsets()) as $fieldSet)
{
echo $this->form->renderFieldset($fieldSet);
} ?>
<div class="control-group">
<div class="controls">
<button type="submit"
class="btn btn-lg btn-primary w-100 akengage-comment-submit-btn"
>
<span class="fa fa-comment-dots" aria-hidden="true"></span>
<?= Text::_('COM_ENGAGE_COMMENTS_FORM_EDIT_BTN_SUBMIT') ?>
</button>
</div>
</div>
<?= $this->loadPosition('engage-after-reply'); ?>
</div>
</form>

View File

@@ -0,0 +1,286 @@
<?php
/**
* @package AkeebaEngage
* @copyright Copyright (c)2020-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') or die();
/**
* View Template for the threaded display of comments.
*
* Loaded from default.php
*/
use Akeeba\Component\Engage\Administrator\Helper\Avatar;
use Akeeba\Component\Engage\Administrator\Helper\UserFetcher;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
// Maximum avatar width, in pixels.
$maxAvatarWidth = 48;
/** @var \Akeeba\Component\Engage\Site\View\Comments\HtmlView $this */
$previousLevel = 0;
$openListItem = 0;
$parentIds = [0 => 0];
$parentNames = [0 => ''];
foreach ($this->items as $comment):
$user = !empty($comment->created_by) && empty($comment->name) ? UserFetcher::getUser($comment->created_by) : new User();
if (empty($comment->created_by) || !empty($comment->name)) {
$user->name = $comment->name;
$user->email = $comment->email;
}
$parentIds[$comment->depth] = $comment->id;
$parentNames[$comment->depth] = $user->name;
// Deeper level comment. Indent with <ul> tags
if ($comment->depth > $previousLevel):
?>
<?php for ($level = $previousLevel + 1; $level <= $comment->depth; $level++): ?>
<ul class="akengage-comment-list akengage-comment-list--level<?= $level ?> list-unstyled">
<?php endfor; ?>
<?php // Shallower level comment. Outdent with </ul> tags
elseif ($comment->depth < $previousLevel): ?>
<?php if ($openListItem): $openListItem--; ?>
</li>
<?php endif; ?>
<?php for ($level = $previousLevel - 1; $level >= $comment->depth; $level--): ?>
</ul>
<?php if ($openListItem): $openListItem--; ?>
</li>
<?php endif; ?>
<?php endfor; ?>
<?php // Same level comment. Close the <li> tag.
else: ?>
<?php $openListItem--; ?>
</li>
<?php endif; ?>
<?php
$previousLevel = $comment->depth;
$avatar = Avatar::getUserAvatar($comment->created_by, $maxAvatarWidth, $comment->email);
$profile = Avatar::getProfileURL($user);
$commentDate = Factory::getDate($comment->created)->setTimezone($this->userTimezone);
$ipLookupURL = $this->getIPLookupURL($comment->ip);
$isModified = !empty($comment->modified_by) && !empty($comment->modified) && (
empty($comment->created_by) || empty($comment->created) || (
($comment->modified_by != $comment->created_by) &&
($comment->modified != $comment->created)
)
);
if ($isModified)
{
if ($comment->modified_by == $comment->created_by)
{
// If the comment is modified by the created by user, use the public name determined at the top of the file.
$modifiedBy = $user->name;
}
else
{
// Someone else modified the comment. Use their name.
$modifiedUser = UserFetcher::getUser($comment->modified_by);
// If the user is no longer available, use '???'
$modifiedBy = ($modifiedUser === null || $modifiedUser->guest) ? Text::_('COM_ENGAGE_LBL_COMMENT_MODIFIED_NO_LONGER_AVAILABLE') : $modifiedUser->name;
}
}
$openListItem++;
$this->ensureHasParentInfo($comment, $parentIds, $parentNames);
$bsCommentStateClass = ($comment->enabled == 1) ? 'secondary' : (($comment->enabled == -3) ? 'warning' : 'danger')
?>
<li class="akengage-comment-item mb-2">
<article
class="akengage-comment--<?= ($comment->enabled == 1) ? 'primary' : (($comment->enabled == -3) ? 'spam' : 'unpublished') ?> border-start border-4 border-<?= $bsCommentStateClass ?> ps-2 mb-2"
id="akengage-comment-<?= $comment->id ?>" itemscope itemtype="http://schema.org/Comment">
<span itemprop="dateCreated" content="<?= $commentDate->toISO8601(false) ?>"></span>
<span itemprop="datePublished" content="<?= $commentDate->toISO8601(false) ?>"></span>
<footer
itemprop="author" itemscope itemtype="http://schema.org/Person"
class="akengage-comment-properties d-flex flex-row gap-1 mb-1 bg-light p-1 small border-bottom border-2">
<?php if (!empty($avatar)): ?>
<div class="akengage-commenter-avatar-container d-none d-sm-block flex-shrink-1" style="max-width: <?= (int) $maxAvatarWidth ?>px">
<?php if (empty($profile)): ?>
<img src="<?= $avatar ?>" alt="" class="akengage-commenter-avatar img-fluid rounded-3 shadow-sm" itemprop="image">
<?php else: ?>
<a href="<?= $profile ?>" class="akengage-commenter-profile" itemprop="url" rel="noopener">
<img src="<?= $avatar ?>"
alt=""
class="akengage-commenter-avatar img-fluid rounded-3 shadow-sm" itemprop="image">
</a>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="akengange-comment-head d-flex flex-column w-100">
<div class="akengange-commenter-name d-flex flex-row flex-wrap gap-3 align-items-center mb-1">
<span itemprop="name" class="fw-bold flex-grow-1"><?= $this->escape($user->name) ?></span>
<?php if ($this->perms['state']): ?>
<div>
<?php if ($user->authorise('core.manage', $comment->asset_id)): ?>
<span class="akengage-commenter-ismoderator fa fa-star text-warning" aria-hidden="true"></span>
<?php elseif (!$user->guest): ?>
<span class="akengage-commenter-isuser fa fa-user text-secondary" aria-hidden="true"></span>
<?php endif; ?>
<?php if (!$user->guest): ?>
<span class="akengage-commenter-username font-monospace text-success"><?= $this->escape($user->username) ?></span>
<?php elseif ($this->perms['state']): ?>
<span class="akengage-commenter-isguest fa fa-user-friends text-danger" aria-hidden="true"></span>
<span class="akengage-commenter-email font-monospace text-muted"><?= $this->escape($user->email) ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="akengage-comment-info d-flex flex-row flex-wrap gap-2 align-items-center">
<div class="akengage-comment-permalink flex-grow-1">
<?php
$tempUri = clone Uri::getInstance();
$tempUri->setFragment(sprintf('akengage-comment-%u', $comment->id));
$tempUri->setVar('akengage_cid', $comment->id);
?>
<a href="<?= $tempUri->toString() ?>"
class="text-body text-decoration-none"
>
<?= $commentDate->format(Text::_('DATE_FORMAT_LC2'), true) ?>
</a>
</div>
<div class="akengage-comment-actions d-flex gap-1">
<?php if ($this->perms['state']): ?>
<span class="akengage-comment-publish_unpublish">
<?php if ($comment->enabled == 1): ?>
<button class="akengage-comment-unpublish-btn btn btn-sm btn-outline-secondary"
data-akengageid="<?= $comment->id ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_UNPUBLISH') ?>
</button>
<?php elseif ($comment->enabled == 0): ?>
<button class="akengage-comment-publish-btn btn btn-sm btn-outline-secondary"
data-akengageid="<?= $comment->id ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_PUBLISH') ?>
</button>
<?php endif; ?>
</span>
<?php if($comment->enabled == -3): ?>
<span class="akengage-comment-mark-ham">
<button class="akengage-comment-markham-btn btn btn-sm btn-outline-success"
data-akengageid="<?= $comment->id ?>"
title="<?= Text::_('COM_ENGAGE_COMMENTS_BTN_MARKHAM_TITLE') ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_MARKHAM') ?>
</button>
</span>
<?php if ($this->perms['delete']): ?>
<span class="akengage-comment-mark-spam">
<button class="akengage-comment-markspam-btn btn btn-sm btn-outline-danger"
data-akengageid="<?= $comment->id ?>"
title="<?= Text::_('COM_ENGAGE_COMMENTS_BTN_MARKSPAM_TITLE') ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_MARKSPAM') ?>
</button>
</span>
<?php endif; ?>
<?php else: ?>
<span class="akengage-comment-mark-possiblespam">
<button class="akengage-comment-possiblespam-btn btn btn-sm btn-outline-warning"
data-akengageid="<?= $comment->id ?>"
title="<?= Text::_('COM_ENGAGE_COMMENTS_BTN_POSSIBLESPAM_TITLE') ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_POSSIBLESPAM') ?>
</button>
</span>
<?php endif; ?>
<?php endif; ?>
<?php if ($this->perms['delete']): ?>
<span class="akengage-comment-delete">
<button class="akengage-comment-delete-btn btn btn-sm btn-outline-danger"
data-akengageid="<?= $comment->id ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_DELETE') ?>
</button>
</span>
<?php endif; ?>
<?php if ($this->perms['edit'] || (($this->user->id === $user->id) && $this->perms['own'])): ?>
<span class="akengage-comment-edit">
<button class="akengage-comment-edit-btn btn btn-sm btn-outline-primary"
data-akengageid="<?= $comment->id ?>">
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_EDIT') ?>
</button>
</span>
<?php endif; ?>
</div>
</div>
<?php if ($this->perms['edit'] || $this->user->authorise('core.manage', $comment->asset_id)): ?>
<div>
<?php if (!empty($ipLookupURL)): ?>
<span class="akengage-comment-ip">
<a href="<?= $ipLookupURL ?>" target="_blank">
<?= Text::sprintf('COM_ENGAGE_COMMENTS_IP', $comment->ip ?? '???') ?>
</a>
</span>
<?php else: ?>
<span class="akengage-comment-ip">
<?= Text::sprintf('COM_ENGAGE_COMMENTS_IP', $comment->ip ?? '???') ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</footer>
<?php if ($comment->enabled == -3): ?>
<div class="akengage-comment-publish-type bg-warning text-white fw-bold p-2">
<?= Text::_('COM_ENGAGE_COMMENTS_TYPE_SPAM') ?>
</div>
<?php elseif ($comment->enabled != 1): ?>
<div class="akengage-comment-publish-type bg-danger text-white fw-bold p-2">
<?= Text::_('COM_ENGAGE_COMMENTS_TYPE_UNPUBLISHED') ?>
</div>
<?php endif ?>
<div class="akengage-comment-body" itemprop="text">
<?= HTMLHelper::_('engage.processCommentTextForDisplay', $comment->body) ?>
<?php if ($isModified): ?>
<div class="my-2 border-top border-1 border-muted text-muted small">
<?= Text::sprintf('COM_ENGAGE_LBL_COMMENT_MODIFIED', Factory::getDate($comment->modified)->setTimezone($this->userTimezone)->format(Text::_('DATE_FORMAT_LC2'), true), $modifiedBy) ?>
</div>
<?php endif; ?>
</div>
<?php if ($this->perms['create']): ?>
<div class="akengage-comment-reply">
<?php // You can reply to $this->maxLevel - 1 level comments only. Replies to deeper nested comments are to the $this->maxLevel - 1 level parent. ?>
<button class="akengage-comment-reply-btn btn btn-sm btn-outline-primary mb-1"
data-akengageid="<?= ($comment->depth < $this->maxLevel) ? $comment->id : $parentIds[$this->maxLevel - 1] ?>"
data-akengagereplyto="<?= $this->escape(($comment->depth < $this->maxLevel) ? $user->name : $parentNames[$this->maxLevel - 1]) ?>"
>
<?= Text::_('COM_ENGAGE_COMMENTS_BTN_REPLY') ?>
</button>
</div>
<?php endif; ?>
</article>
<?php endforeach; ?>
<?php if ($openListItem): ?>
<?php $openListItem--; ?>
</li>
<?php endif; ?>
<?php for ($level = $previousLevel; $level >= 1; $level--): ?>
</ul>
<?php if ($openListItem): ?>
<?php $openListItem--; ?>
</li>
<?php endif; ?>
<?php endfor; ?>

View File

@@ -0,0 +1,45 @@
<?php
/**
* @package AkeebaEngage
* @copyright Copyright (c)2020-2025 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
defined('_JEXEC') or die();
/**
* View Template for the guest users login form
*
* Loaded from default.php
*/
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Language\Text;
/** @var \Akeeba\Component\Engage\Site\View\Comments\HtmlView $this */
$cParams = ComponentHelper::getParams('com_engage');
$loginModule = $cParams->get('login_module', '-1');
$moduleContent = (empty($loginModule) || ($loginModule === '-1')) ? '' : trim($this->loadModule($loginModule));
$positionContent = trim($this->loadPosition('engage-login'));
/**
* A reason for this to happen is that site owner wants discussion to be open to invitation-only members of the site but
* visible by anyone. This is mostly relevant in political organizations, NGOs and local / closed community
* organizations where a small number of people are openly discussing a public interest issue, but they don't want to
* allow random people to detract the conversation.
*/
if (empty($moduleContent) && empty($positionContent))
{
return;
}
?>
<footer id="akeeba-engage-login">
<h4>
<?= Text::_('COM_ENGAGE_COMMENTS_LOGIN_HEAD') ?>
</h4>
<?= $moduleContent ?>
<?= $positionContent ?>
</footer>

View File

@@ -0,0 +1,118 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,118 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,195 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
/**
* Layout variables
*
* @var array $items
* @var MPFInput $input
* @var MPFConfig $config
* @var int $Itemid
* @var int $categoryId
* @var OSMembershipHelperBootstrap $bootstrapHelper
*/
$rootUri = Uri::root(true);
$subscribedPlanIds = OSMembershipHelperSubscription::getSubscribedPlans();
if (!isset($categoryId))
{
$categoryId = 0;
}
$rowFluidClass = $bootstrapHelper->getClassMapping('row-fluid');
$span7Class = $bootstrapHelper->getClassMapping('span7');
$span5class = $bootstrapHelper->getClassMapping('span5');
$imgClass = $bootstrapHelper->getClassMapping('img-polaroid');
$btnClass = $bootstrapHelper->getClassMapping('btn');
$btnPrimaryClass = $bootstrapHelper->getClassMapping('btn btn-primary');
$clearfixClass = $bootstrapHelper->getClassMapping('clearfix');
$defaultItemId = $Itemid;
for ($i = 0 , $n = count($items) ; $i < $n ; $i++)
{
$item = $items[$i];
$Itemid = OSMembershipHelperRoute::getPlanMenuId($item->id, $item->category_id, $defaultItemId);
if ($item->thumb)
{
$imgSrc = $rootUri . '/media/com_osmembership/' . $item->thumb;
}
if ($item->category_id)
{
$url = Route::_('index.php?option=com_osmembership&view=plan&catid=' . $item->category_id . '&id=' . $item->id . '&Itemid=' . $Itemid);
}
else
{
$url = Route::_('index.php?option=com_osmembership&view=plan&id=' . $item->id . '&Itemid=' . $Itemid);
}
if ($config->use_https)
{
$signUpUrl = Route::_(OSMembershipHelperRoute::getSignupRoute($item->id, $Itemid), false, 1);
}
else
{
$signUpUrl = Route::_(OSMembershipHelperRoute::getSignupRoute($item->id, $Itemid));
}
$symbol = $item->currency_symbol ?: $item->currency;
?>
<div class="osm-item-wrapper <?php echo $clearfixClass; ?>">
<div class="osm-item-heading-box <?php echo $clearfixClass; ?>">
<h3 class="osm-item-title">
<a href="<?php echo $url; ?>" title="<?php echo $item->title; ?>">
<?php echo $item->title; ?>
</a>
</h3>
</div>
<div class="osm-item-description <?php echo $clearfixClass; ?>">
<div class="<?php echo $rowFluidClass; ?>">
<div class="osm-description-details <?php echo $span7Class; ?>">
<?php
if ($item->thumb)
{
?>
<img src="<?php echo $imgSrc; ?>" alt="<?php echo $item->title; ?>" class="osm-thumb-left <?php echo $imgClass; ?>"/>
<?php
}
if ($item->short_description)
{
echo $item->short_description;
}
else
{
echo $item->description;
}
?>
</div>
<div class="<?php echo $span5class; ?>">
<?php echo OSMembershipHelperHtml::loadCommonLayout('common/tmpl/plan_information.php', ['item' => $item]); ?>
</div>
</div>
<div class="osm-taskbar <?php echo $clearfixClass; ?>">
<ul>
<?php
$actions = OSMembershipHelperSubscription::getAllowedActions($item);
if (count($actions))
{
$language = Factory::getApplication()->getLanguage();
if (in_array('subscribe', $actions))
{
if ($language->hasKey('OSM_SIGNUP_PLAN_' . $item->id))
{
$signUpLanguageItem = 'OSM_SIGNUP_PLAN_' . $item->id;
}
else
{
$signUpLanguageItem = 'OSM_SIGNUP';
}
if ($language->hasKey('OSM_RENEW_PLAN_' . $item->id))
{
$renewLanguageItem = 'OSM_RENEW_PLAN_' . $item->id;
}
else
{
$renewLanguageItem = 'OSM_RENEW';
}
?>
<li>
<a href="<?php echo $signUpUrl; ?>" class="<?php echo $btnPrimaryClass; ?>">
<?php echo in_array($item->id, $subscribedPlanIds) ? Text::_($renewLanguageItem) : Text::_($signUpLanguageItem); ?>
</a>
</li>
<?php
}
if (in_array('upgrade', $actions))
{
if ($language->hasKey('OSM_UPGRADE_PLAN_' . $item->id))
{
$upgradeLanguageItem = 'OSM_UPGRADE_PLAN_' . $item->id;
}
else
{
$upgradeLanguageItem = 'OSM_UPGRADE';
}
if (count($item->upgrade_rules) > 1)
{
$link = Route::_('index.php?option=com_osmembership&view=upgrademembership&to_plan_id=' . $item->id . '&Itemid=' . OSMembershipHelperRoute::findView('upgrademembership', $Itemid));
}
else
{
$upgradeOptionId = $item->upgrade_rules[0]->id;
$link = Route::_('index.php?option=com_osmembership&task=register.process_upgrade_membership&upgrade_option_id=' . $upgradeOptionId . '&Itemid=' . $Itemid);
}
?>
<li>
<a href="<?php echo $link; ?>" class="<?php echo $btnPrimaryClass; ?>">
<?php echo Text::_($upgradeLanguageItem); ?>
</a>
</li>
<?php
}
}
if (empty($config->hide_details_button))
{
?>
<li>
<a href="<?php echo $url; ?>" class="<?php echo $btnClass; ?>">
<?php echo Text::_('OSM_DETAILS'); ?>
</a>
</li>
<?php
}
?>
</ul>
</div>
</div>
</div>
<?php
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
/**
* Layout variables
*
* @var string $selector
* @var string $title
*/
defined('_JEXEC') or die;
Factory::getApplication()
->getDocument()
->getWebAssetManager()
->useScript('core');
Text::script('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST');
$message = "alert(Joomla.JText._('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST'));";
?>
<button type="button" data-toggle="modal" onclick="if (document.adminForm.boxchecked.value==0){<?php echo $message; ?>}else{jQuery( '#<?php echo $selector; ?>' ).modal('show'); return true;}" class="btn btn-small">
<span class="icon-checkbox-partial" aria-hidden="true"></span>
<?php echo $title; ?>
</button>

View File

@@ -0,0 +1,24 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2010 - 2022 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
/**
* Layout variables
*
* @var string $selector
* @var string $title
*/
?>
<button type="button" data-toggle="modal" onclick="jQuery( '#<?php echo $selector; ?>' ).modal('show'); return true;" class="btn btn-small">
<span class="icon-checkbox-partial" aria-hidden="true"></span>
<?php echo $title; ?>
</button>

View File

@@ -0,0 +1,56 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
/**
* Layout variables
*
* @var array $items
* @var MPFConfig $config
* @var int $categoryId
* @var int $Itemid
*/
$bootstrapHelper = OSMembershipHelperBootstrap::getInstance();
$clearfixClass = $bootstrapHelper->getClassMapping('clearfix');
for ($i = 0 , $n = count($items) ; $i < $n ; $i++)
{
$item = $items[$i];
$link = Route::_(OSMembershipHelperRoute::getCategoryRoute($item->id, $Itemid));
?>
<div class="osm-item-wrapper clearfix">
<div class="osm-item-heading-box">
<h3 class="osm-item-title">
<a href="<?php echo $link; ?>" class="osm-item-title-link">
<?php echo $item->title;?>
</a>
<span class="<?php echo $bootstrapHelper->getClassMapping('badge badge-info'); ?>"><?php echo $item->total_plans ;?> <?php echo $item->total_plans > 1 ? Text::_('OSM_PLANS') : Text::_('OSM_PLAN') ; ?></span>
</h3>
</div>
<?php
if($item->description)
{
?>
<div class="osm-item-description <?php echo $clearfixClass; ?>">
<?php echo HTMLHelper::_('content.prepare', $item->description);?>
</div>
<?php
}
?>
</div>
<?php
}

View File

@@ -0,0 +1,219 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
/**
* Layout variables
*
* @var array $items
* @var int $categoryId
* @var MPFInput $input
* @var MPFConfig $config
* @var OSMembershipHelperBootstrap $bootstrapHelper
* @var \Joomla\Registry\Registry $params
* @var int $Itemid
*/
Factory::getApplication()
->getDocument()
->getWebAssetManager()
->useScript('core');
$rootUri = Uri::root(true);
$minHeight = 130;
if (isset($params))
{
$minHeight = (int) $params->get('min_height', 130) ?: 130;
}
OSMembershipHelperJquery::responsiveEqualHeight('.osm-item-description-text', $minHeight);
$subscribedPlanIds = OSMembershipHelperSubscription::getSubscribedPlans();
if (isset($input) && $input->getInt('number_columns'))
{
$numberColumns = $input->getInt('number_columns');
}
elseif (!empty($config->number_columns))
{
$numberColumns = $config->number_columns;
}
else
{
$numberColumns = 3;
}
if (!isset($categoryId))
{
$categoryId = 0;
}
$span = intval(12 / $numberColumns);
$btnClass = $bootstrapHelper->getClassMapping('btn');
$btnPrimaryClass = $bootstrapHelper->getClassMapping('btn btn-primary');
$imgClass = $bootstrapHelper->getClassMapping('img-polaroid');
$spanClass = $bootstrapHelper->getClassMapping('span' . $span);
$rowFluidClearfixClass = $bootstrapHelper->getClassMapping('row-fluid clearfix');
$clearFixClass = $bootstrapHelper->getClassMapping('clearfix');
?>
<div class="<?php echo $rowFluidClearfixClass; ?>">
<?php
$i = 0;
$numberPlans = count($items);
$defaultItemId = $Itemid;
foreach ($items as $item)
{
$i++;
$Itemid = OSMembershipHelperRoute::getPlanMenuId($item->id, $item->category_id, $defaultItemId);
if ($item->thumb)
{
$imgSrc = $rootUri . '/media/com_osmembership/' . $item->thumb;
}
$url = Route::_('index.php?option=com_osmembership&view=plan&catid=' . $item->category_id . '&id=' . $item->id . '&Itemid=' . $Itemid);
if ($config->use_https)
{
$signUpUrl = Route::_(OSMembershipHelperRoute::getSignupRoute($item->id, $Itemid), false, 1);
}
else
{
$signUpUrl = Route::_(OSMembershipHelperRoute::getSignupRoute($item->id, $Itemid));
}
?>
<div class="osm-item-wrapper <?php echo $spanClass; ?>">
<div class="osm-item-heading-box <?php echo $clearFixClass; ?>">
<h2 class="osm-item-title">
<a href="<?php echo $url; ?>" title="<?php echo $item->title; ?>">
<?php echo $item->title; ?>
</a>
</h2>
</div>
<div class="osm-item-description <?php echo $clearFixClass; ?>">
<?php
if ($item->thumb)
{
?>
<a href="<?php echo $url; ?>" title="<?php echo $item->title; ?>">
<img src="<?php echo $imgSrc; ?>" class="osm-thumb-left <?php echo $imgClass; ?>" />
</a>
<?php
}
if (!$item->short_description)
{
$item->short_description = $item->description;
}
?>
<div class="osm-item-description-text"><?php echo $item->short_description; ?></div>
<div class="osm-taskbar <?php echo $clearFixClass; ?>">
<ul>
<?php
$actions = OSMembershipHelperSubscription::getAllowedActions($item);
if (count($actions))
{
$language = Factory::getApplication()->getLanguage();
if (in_array('subscribe', $actions))
{
if ($language->hasKey('OSM_SIGNUP_PLAN_' . $item->id))
{
$signUpLanguageItem = 'OSM_SIGNUP_PLAN_' . $item->id;
}
else
{
$signUpLanguageItem = 'OSM_SIGNUP';
}
if ($language->hasKey('OSM_RENEW_PLAN_' . $item->id))
{
$renewLanguageItem = 'OSM_RENEW_PLAN_' . $item->id;
}
else
{
$renewLanguageItem = 'OSM_RENEW';
}
?>
<li>
<a href="<?php echo $signUpUrl; ?>" class="<?php echo $btnPrimaryClass; ?>">
<?php echo in_array($item->id, $subscribedPlanIds) ? Text::_($renewLanguageItem) : Text::_($signUpLanguageItem); ?>
</a>
</li>
<?php
}
if (in_array('upgrade', $actions))
{
if ($language->hasKey('OSM_UPGRADE_PLAN_' . $item->id))
{
$upgradeLanguageItem = 'OSM_UPGRADE_PLAN_' . $item->id;
}
else
{
$upgradeLanguageItem = 'OSM_UPGRADE';
}
if (count($item->upgrade_rules) > 1)
{
$link = Route::_('index.php?option=com_osmembership&view=upgrademembership&to_plan_id=' . $item->id . '&Itemid=' . OSMembershipHelperRoute::findView('upgrademembership', $Itemid));
}
else
{
$upgradeOptionId = $item->upgrade_rules[0]->id;
$link = Route::_('index.php?option=com_osmembership&task=register.process_upgrade_membership&upgrade_option_id=' . $upgradeOptionId . '&Itemid=' . $Itemid);
}
?>
<li>
<a href="<?php echo $link; ?>" class="<?php echo $btnPrimaryClass; ?>">
<?php echo Text::_($upgradeLanguageItem); ?>
</a>
</li>
<?php
}
}
if (empty($config->hide_details_button))
{
?>
<li>
<a href="<?php echo $url; ?>" class="<?php echo $btnClass; ?>">
<?php echo Text::_('OSM_DETAILS'); ?>
</a>
</li>
<?php
}
?>
</ul>
</div>
</div>
</div>
<?php
if ($i % $numberColumns == 0 && $i < $numberPlans)
{
?>
</div>
<div class="<?php echo $rowFluidClearfixClass; ?>">
<?php
}
}
?>
</div>

View File

@@ -0,0 +1,231 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
/**
* Layout variables
*
* @var array $items
* @var int $categoryId
* @var MPFInput $input
* @var MPFConfig $config
* @var OSMembershipHelperBootstrap $bootstrapHelper
* @var \Joomla\Registry\Registry $params
* @var int $Itemid
*/
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
$rootUri = Uri::root(true);
$subscribedPlanIds = OSMembershipHelperSubscription::getSubscribedPlans();
if (!isset($categoryId))
{
$categoryId = 0;
}
$rowFluidClass = $bootstrapHelper->getClassMapping('row-fluid');
$imgClass = $bootstrapHelper->getClassMapping('img-polaroid');
$btnClass = $bootstrapHelper->getClassMapping('btn');
$btnPrimaryClass = $bootstrapHelper->getClassMapping('btn btn-primary');
$clearfixClass = $bootstrapHelper->getClassMapping('clearfix');
$defaultItemId = $Itemid;
if (isset($params))
{
$showPlanInformation = $params->get('show_plan_information', 1);
$planInformationPosition = $params->get('plan_information_position', 0);
}
else
{
$showPlanInformation = 1;
$planInformationPosition = 0;
}
if ($showPlanInformation && $planInformationPosition == 0)
{
$leftClass = $bootstrapHelper->getClassMapping('span7');
$rightClass = $bootstrapHelper->getClassMapping('span5');
}
else
{
$leftClass = $bootstrapHelper->getClassMapping('clearfix');
$rightClass = $bootstrapHelper->getClassMapping('clearfix');
}
for ($i = 0 , $n = count($items) ; $i < $n ; $i++)
{
$item = $items[$i];
$Itemid = OSMembershipHelperRoute::getPlanMenuId($item->id, $item->category_id, $defaultItemId);
if ($item->thumb)
{
$imgSrc = $rootUri . '/media/com_osmembership/' . $item->thumb;
}
if ($item->category_id)
{
$url = Route::_('index.php?option=com_osmembership&view=plan&catid=' . $item->category_id . '&id=' . $item->id . '&Itemid=' . $Itemid);
}
else
{
$url = Route::_('index.php?option=com_osmembership&view=plan&id=' . $item->id . '&Itemid=' . $Itemid);
}
if ($config->use_https)
{
$signUpUrl = Route::_(OSMembershipHelperRoute::getSignupRoute($item->id, $Itemid), false, 1);
}
else
{
$signUpUrl = Route::_(OSMembershipHelperRoute::getSignupRoute($item->id, $Itemid));
}
?>
<div class="osm-item-wrapper <?php echo $clearfixClass; ?>">
<div class="osm-item-heading-box <?php echo $clearfixClass; ?>">
<h2 class="osm-item-title">
<a href="<?php echo $url; ?>" title="<?php echo $item->title; ?>">
<?php echo $item->title; ?>
</a>
</h2>
</div>
<div class="osm-item-description <?php echo $clearfixClass; ?>">
<div class="<?php echo $rowFluidClass; ?>">
<?php
if ($showPlanInformation && $planInformationPosition == 1)
{
?>
<div class="<?php echo $rightClass; ?>">
<?php echo OSMembershipHelperHtml::loadCommonLayout('common/tmpl/plan_information.php', ['item' => $item]); ?>
</div>
<?php
}
?>
<div class="osm-description-details <?php echo $leftClass; ?>">
<?php
if ($item->thumb)
{
?>
<img src="<?php echo $imgSrc; ?>" alt="<?php echo $item->title; ?>" class="osm-thumb-left <?php echo $imgClass; ?>"/>
<?php
}
if ($item->short_description)
{
echo $item->short_description;
}
else
{
echo $item->description;
}
?>
</div>
<?php
if ($showPlanInformation && in_array($planInformationPosition, [0, 2]))
{
?>
<div class="<?php echo $rightClass; ?>">
<?php echo OSMembershipHelperHtml::loadCommonLayout('common/tmpl/plan_information.php', ['item' => $item]); ?>
</div>
<?php
}
?>
</div>
<div class="osm-taskbar <?php echo $clearfixClass; ?>">
<ul>
<?php
$actions = OSMembershipHelperSubscription::getAllowedActions($item);
if (count($actions))
{
$language = Factory::getApplication()->getLanguage();
if (in_array('subscribe', $actions))
{
if ($language->hasKey('OSM_SIGNUP_PLAN_' . $item->id))
{
$signUpLanguageItem = 'OSM_SIGNUP_PLAN_' . $item->id;
}
else
{
$signUpLanguageItem = 'OSM_SIGNUP';
}
if ($language->hasKey('OSM_RENEW_PLAN_' . $item->id))
{
$renewLanguageItem = 'OSM_RENEW_PLAN_' . $item->id;
}
else
{
$renewLanguageItem = 'OSM_RENEW';
}
?>
<li>
<a href="<?php echo $signUpUrl; ?>" class="<?php echo $btnPrimaryClass; ?>">
<?php echo in_array($item->id, $subscribedPlanIds) ? Text::_($renewLanguageItem) : Text::_($signUpLanguageItem); ?>
</a>
</li>
<?php
}
if (in_array('upgrade', $actions))
{
if ($language->hasKey('OSM_UPGRADE_PLAN_' . $item->id))
{
$upgradeLanguageItem = 'OSM_UPGRADE_PLAN_' . $item->id;
}
else
{
$upgradeLanguageItem = 'OSM_UPGRADE';
}
if (count($item->upgrade_rules) > 1)
{
$link = Route::_('index.php?option=com_osmembership&view=upgrademembership&to_plan_id=' . $item->id . '&Itemid=' . OSMembershipHelperRoute::findView('upgrademembership', $Itemid));
}
else
{
$upgradeOptionId = $item->upgrade_rules[0]->id;
$link = Route::_('index.php?option=com_osmembership&task=register.process_upgrade_membership&upgrade_option_id=' . $upgradeOptionId . '&Itemid=' . $Itemid);
}
?>
<li>
<a href="<?php echo $link; ?>" class="<?php echo $btnPrimaryClass; ?>">
<?php echo Text::_($upgradeLanguageItem); ?>
</a>
</li>
<?php
}
}
if (empty($config->hide_details_button))
{
?>
<li>
<a href="<?php echo $url; ?>" class="<?php echo $btnClass; ?>">
<?php echo Text::_('OSM_DETAILS'); ?>
</a>
</li>
<?php
}
?>
</ul>
</div>
</div>
</div>
<?php
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
/**
* Layout variables
*
* @var array $rowMembers
*/
$names = [];
foreach ($rowMembers as $rowMember)
{
$names[] = trim($rowMember->first_name . ' ' . $rowMember->last_name);
}
echo implode("\r\n", $names);

View File

@@ -0,0 +1,118 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,36 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
/**
* Layout variables
*
* @var string $redirectHeading
* @var string $url
* @var bool $newWindow
* @var array $data
*/
?>
<div class="payment-heading"><?php echo $redirectHeading; ?></div>
<form method="post" action="<?php echo $url; ?>" name="payment_form"
id="payment_form"<?php if ($newWindow) echo ' target="_blank"'; ?>>
<?php
foreach ($data as $key => $val)
{
echo '<input type="hidden" name="' . $key . '" value="' . $val . '" />';
echo "\n";
}
?>
<script type="text/javascript">
document.payment_form.submit();
</script>
</form>

View File

@@ -0,0 +1,45 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
/**
* Layout variables
*
* @var stdClass $item
*/
use Joomla\CMS\Form\Form;
use Joomla\CMS\Language\Text;
try
{
$form = Form::getInstance('plan_fields', JPATH_ROOT . '/components/com_osmembership/fields.xml', [], false, '//config');
}
catch (Exception $e)
{
return;
}
foreach ($form->getFieldset('basic') as $field)
{
if ($field->getAttribute('hide'))
{
continue;
}
?>
<tr class="osm-plan-property">
<td class="osm-plan-property-label">
<?php echo Text::_($field->getAttribute('label')); ?>:
</td>
<td class="osm-plan-property-value">
<?php echo $item->fieldsData->get($field->getAttribute('name')); ?>
</td>
</tr>
<?php
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
/**
* Layout variables
*
* @var stdClass $item
*/
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
$bootstrapHelper = OSMembershipHelperBootstrap::getInstance();
$config = OSMembershipHelper::getConfig();
$symbol = $item->currency_symbol ?: $item->currency;
?>
<table class="<?php echo $bootstrapHelper->getClassMapping('table table-striped table-bordered'); ?>">
<?php
if ($item->setup_fee > 0)
{
?>
<tr class="osm-plan-property">
<td class="osm-plan-property-label">
<?php echo Text::_('OSM_SETUP_FEE'); ?>:
</td>
<td class="osm-plan-property-value">
<?php echo OSMembershipHelper::formatCurrency($item->setup_fee, $config, $symbol); ?>
</td>
</tr>
<?php
}
if ($item->recurring_subscription && $item->trial_duration)
{
?>
<tr class="osm-plan-property">
<td class="osm-plan-property-label">
<?php echo Text::_('OSM_TRIAL_DURATION'); ?>:
</td>
<td class="osm-plan-property-value">
<?php
if ($item->lifetime_membership)
{
echo Text::_('OSM_LIFETIME');
}
else
{
echo OSMembershipHelperSubscription::getDurationText($item->trial_duration, $item->trial_duration_unit);
}
?>
</td>
</tr>
<tr class="osm-plan-property">
<td class="osm-plan-property-label">
<?php echo Text::_('OSM_TRIAL_PRICE'); ?>:
</td>
<td class="osm-plan-property-value">
<?php
if ($item->trial_amount > 0)
{
echo OSMembershipHelper::formatCurrency($item->trial_amount, $config, $symbol);
}
else
{
echo Text::_('OSM_FREE');
}
?>
</td>
</tr>
<?php
}
if (!((int) $item->expired_date))
{
?>
<tr class="osm-plan-property">
<td class="osm-plan-property-label">
<?php echo Text::_('OSM_DURATION'); ?>:
</td>
<td class="osm-plan-property-value">
<?php
if ($item->lifetime_membership)
{
echo Text::_('OSM_LIFETIME');
}
else
{
echo OSMembershipHelperSubscription::getDurationText($item->subscription_length, $item->subscription_length_unit);
}
?>
</td>
</tr>
<?php
}
?>
<tr class="osm-plan-property">
<td class="osm-plan-property-label">
<?php echo Text::_('OSM_PRICE'); ?>:
</td>
<td class="osm-plan-property-value">
<?php
if ($item->price > 0)
{
echo OSMembershipHelper::formatCurrency($item->price, $config, $symbol);
}
else
{
echo Text::_('OSM_FREE');
}
?>
</td>
</tr>
<?php
if (file_exists(JPATH_ROOT . '/components/com_osmembership/fields.xml')
&& filesize(JPATH_ROOT . '/components/com_osmembership/fields.xml'))
{
echo OSMembershipHelperHtml::loadCommonLayout('common/tmpl/plan_custom_fields.php', ['item' => $item]);
}
?>
</table>

View File

@@ -0,0 +1,70 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
/**
* Layout variables
*
* @var stdClass $item
*/
$config = OSMembershipHelper::getConfig();
$dec_point = $config->dec_point ?? '.';
$thousands_sep = $config->thousands_sep ?? ',';
if ($item->lifetime_membership)
{
$subscriptionLengthText = Text::_('OSM_LIFETIME');
}
else
{
$subscriptionLengthText = OSMembershipHelperSubscription::getDurationText($item->subscription_length, $item->subscription_length_unit, false);
}
if ($item->price > 0)
{
$priceParts = explode('.', $item->price);
if ($priceParts[1] == '00' || $config->decimals === '0')
{
$numberDecimals = 0;
}
else
{
$numberDecimals = 2;
}
$symbol = $item->currency_symbol ?: $item->currency;
if (!$symbol)
{
$symbol = $config->currency_symbol;
}
if ($config->currency_position == 0)
{
echo $symbol . number_format($item->price, $numberDecimals, $dec_point, $thousands_sep) . ($subscriptionLengthText ? "<sub>/$subscriptionLengthText</sub>" : '');
}
else
{
echo number_format($item->price, $numberDecimals, $dec_point, $thousands_sep) . $symbol . ($subscriptionLengthText ? "<sub>/$subscriptionLengthText</sub>" : '');
}
}
else
{
echo Text::_('OSM_FREE') . ($subscriptionLengthText ? "<sub> /$subscriptionLengthText</sub>" : '');
}

View File

@@ -0,0 +1,237 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
/**
* Layout variables
*
* @var array $items
* @var MPFInput $input
* @var MPFConfig $config
* @var int $Itemid
* @var int $categoryId
* @var OSMembershipHelperBootstrap $bootstrapHelper
*/
$rootUri = Uri::root(true);
$subscribedPlanIds = OSMembershipHelperSubscription::getSubscribedPlans();
if (empty($params))
{
$params = Factory::getApplication()->getParams();
}
if (isset($input) && $input->getInt('recommended_plan_id'))
{
$recommendedPlanId = $input->getInt('recommended_plan_id');
}
else
{
$recommendedPlanId = (int) $params->get('recommended_campaign_id');
}
$standardPlanBackgroundColor = $params->get('standard_plan_color', '#00B69C');
$recommendedPlanBackgroundColor = $params->get('recommended_plan_color', '#bF75500');
$showDetailsButton = $params->get('show_details_button', 0);
if (isset($input) && $input->getInt('number_columns'))
{
$numberColumns = $input->getInt('number_columns');
}
elseif (isset($config->number_columns))
{
$numberColumns = $config->number_columns;
}
else
{
$numberColumns = 3;
}
$numberColumns = min($numberColumns, 5);
if (!isset($categoryId))
{
$categoryId = 0;
}
$span = intval(12 / $numberColumns);
$imgClass = $bootstrapHelper->getClassMapping('img-polaroid');
$spanClass = $bootstrapHelper->getClassMapping('span' . $span);
$i = 0;
$numberPlans = count($items);
$defaultItemId = $Itemid;
$rootUri = Uri::root(true);
foreach ($items as $item)
{
$Itemid = OSMembershipHelperRoute::getPlanMenuId($item->id, $item->category_id, $defaultItemId);
if ($item->thumb)
{
$imgSrc = $rootUri . '/media/com_osmembership/' . $item->thumb;
}
$url = Route::_('index.php?option=com_osmembership&view=plan&catid=' . $item->category_id . '&id=' . $item->id . '&Itemid=' . $Itemid);
if ($config->use_https)
{
$signUpUrl = Route::_(OSMembershipHelperRoute::getSignupRoute($item->id, $Itemid), false, 1);
}
else
{
$signUpUrl = Route::_(OSMembershipHelperRoute::getSignupRoute($item->id, $Itemid));
}
if (!$item->short_description)
{
$item->short_description = $item->description;
}
if ($item->id == $recommendedPlanId)
{
$recommended = true;
$backgroundColor = $recommendedPlanBackgroundColor;
}
else
{
$recommended = false;
$backgroundColor = $standardPlanBackgroundColor;
}
if ($i % $numberColumns == 0)
{
?>
<div class="<?php echo $bootstrapHelper->getClassMapping('row-fluid clearfix'); ?> osm-pricing-table-circle">
<?php
}
?>
<div class="<?php echo $spanClass; ?>">
<div class="osm-plan osm-plan-<?php echo $item->id; ?>">
<div class="osm-plan-header" style="background-color: <?php echo $backgroundColor; ?>">
<h2 class="osm-plan-title">
<?php echo $item->title; ?>
</h2>
<div class="osm-plan-price" style="background-color: <?php echo $backgroundColor; ?>">
<p class="price">
<?php echo OSMembershipHelperHtml::loadCommonLayout('common/tmpl/priceduration.php', ['item' => $item]); ?>
</p>
</div>
</div>
<div class="osm-plan-short-description">
<?php echo $item->short_description;?>
</div>
<ul class="osm-signup-container">
<?php
$actions = OSMembershipHelperSubscription::getAllowedActions($item);
if(count($actions))
{
$language = Factory::getApplication()->getLanguage();
if (in_array('subscribe', $actions))
{
if ($language->hasKey('OSM_SIGNUP_PLAN_' . $item->id))
{
$signUpLanguageItem = 'OSM_SIGNUP_PLAN_' . $item->id;
}
else
{
$signUpLanguageItem = 'OSM_SIGNUP';
}
if ($language->hasKey('OSM_RENEW_PLAN_' . $item->id))
{
$renewLanguageItem = 'OSM_RENEW_PLAN_' . $item->id;
}
else
{
$renewLanguageItem = 'OSM_RENEW';
}
?>
<li>
<a href="<?php echo $signUpUrl; ?>" class="btn-signup" style="background-color: <?php echo $backgroundColor; ?>">
<?php echo in_array($item->id, $subscribedPlanIds) ? Text::_($renewLanguageItem) : Text::_($signUpLanguageItem); ?>
</a>
</li>
<?php
}
if (in_array('upgrade', $actions))
{
if ($language->hasKey('OSM_UPGRADE_PLAN_' . $item->id))
{
$upgradeLanguageItem = 'OSM_UPGRADE_PLAN_' . $item->id;
}
else
{
$upgradeLanguageItem = 'OSM_UPGRADE';
}
if (count($item->upgrade_rules) > 1)
{
$link = Route::_('index.php?option=com_osmembership&view=upgrademembership&to_plan_id=' . $item->id . '&Itemid=' . OSMembershipHelperRoute::findView('upgrademembership', $Itemid));
}
else
{
$upgradeOptionId = $item->upgrade_rules[0]->id;
$link = Route::_('index.php?option=com_osmembership&task=register.process_upgrade_membership&upgrade_option_id=' . $upgradeOptionId . '&Itemid=' . $Itemid);
}
?>
<li>
<a href="<?php echo $link; ?>" class="btn-signup" style="background-color: <?php echo $backgroundColor; ?>">
<?php echo Text::_($upgradeLanguageItem); ?>
</a>
</li>
<?php
}
}
if ($showDetailsButton)
{
?>
<li>
<a href="<?php echo $url; ?>" class="btn-signup" style="background-color: <?php echo $backgroundColor; ?>">
<?php echo Text::_('OSM_DETAILS'); ?>
</a>
</li>
<?php
}
?>
</ul>
</div>
</div>
<?php
if (($i + 1) % $numberColumns == 0)
{
?>
</div>
<?php
}
$i++;
}
if ($i % $numberColumns != 0)
{
echo '</div>' ;
}
?>
<style type="text/css">
.osm-pricing-table-circle .osm-plan:hover .osm-plan-price {
background-color: <?php echo $recommendedPlanBackgroundColor; ?>!important;
}
</style>

View File

@@ -0,0 +1,233 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
/**
* Layout variables
*
* @var array $items
* @var MPFInput $input
* @var MPFConfig $config
* @var int $Itemid
* @var int $categoryId
* @var OSMembershipHelperBootstrap $bootstrapHelper
*/
// Load equals height script
$rootUri = Uri::root(true);
$subscribedPlanIds = OSMembershipHelperSubscription::getSubscribedPlans();
if (empty($params))
{
$params = Factory::getApplication()->getParams();
}
if (isset($input) && $input->getInt('recommended_plan_id'))
{
$recommendedPlanId = $input->getInt('recommended_plan_id');
}
else
{
$recommendedPlanId = (int) $params->get('recommended_campaign_id');
}
$standardPlanBackgroundColor = $params->get('standard_plan_color', '#00B69C');
$recommendedPlanBackgroundColor = $params->get('recommended_plan_color', '#F75500');
$showDetailsButton = $params->get('show_details_button', 0);
if (isset($input) && $input->getInt('number_columns'))
{
$numberColumns = $input->getInt('number_columns');
}
elseif (isset($config->number_columns))
{
$numberColumns = $config->number_columns;
}
else
{
$numberColumns = 3;
}
$numberColumns = min($numberColumns, 5);
if (!isset($categoryId))
{
$categoryId = 0;
}
$span = intval(12 / $numberColumns);
$imgClass = $bootstrapHelper->getClassMapping('img-polaroid');
$spanClass = $bootstrapHelper->getClassMapping('span' . $span);
$i = 0;
$numberPlans = count($items);
$defaultItemId = $Itemid;
$rootUri = Uri::root(true);
foreach ($items as $item)
{
$Itemid = OSMembershipHelperRoute::getPlanMenuId($item->id, $item->category_id, $defaultItemId);
if ($item->thumb)
{
$imgSrc = $rootUri . '/media/com_osmembership/' . $item->thumb;
}
$url = Route::_('index.php?option=com_osmembership&view=plan&catid=' . $item->category_id . '&id=' . $item->id . '&Itemid=' . $Itemid);
if ($config->use_https)
{
$signUpUrl = Route::_(OSMembershipHelperRoute::getSignupRoute($item->id, $Itemid), false, 1);
}
else
{
$signUpUrl = Route::_(OSMembershipHelperRoute::getSignupRoute($item->id, $Itemid));
}
if (!$item->short_description)
{
$item->short_description = $item->description;
}
if ($item->id == $recommendedPlanId)
{
$recommended = true;
$backgroundColor = $recommendedPlanBackgroundColor;
}
else
{
$recommended = false;
$backgroundColor = $standardPlanBackgroundColor;
}
if ($i % $numberColumns == 0)
{
?>
<div class="<?php echo $bootstrapHelper->getClassMapping('row-fluid clearfix'); ?> osm-pricing-table-flat">
<?php
}
?>
<div class="<?php echo $spanClass; ?>">
<div class="osm-plan osm-plan-<?php echo $item->id; ?>" style="background-color: <?php echo $backgroundColor; ?>">
<div class="osm-plan-header">
<h2 class="osm-plan-title">
<?php echo $item->title; ?>
</h2>
</div>
<div class="osm-plan-price">
<p class="price">
<?php echo OSMembershipHelperHtml::loadCommonLayout('common/tmpl/priceduration.php', ['item' => $item]); ?>
</p>
</div>
<div class="osm-plan-short-description">
<?php echo $item->short_description;?>
</div>
<ul class="osm-signup-container">
<?php
$actions = OSMembershipHelperSubscription::getAllowedActions($item);
if (count($actions))
{
$language = Factory::getApplication()->getLanguage();
if (in_array('subscribe', $actions))
{
if ($language->hasKey('OSM_SIGNUP_PLAN_' . $item->id))
{
$signUpLanguageItem = 'OSM_SIGNUP_PLAN_' . $item->id;
}
else
{
$signUpLanguageItem = 'OSM_SIGNUP';
}
if ($language->hasKey('OSM_RENEW_PLAN_' . $item->id))
{
$renewLanguageItem = 'OSM_RENEW_PLAN_' . $item->id;
}
else
{
$renewLanguageItem = 'OSM_RENEW';
}
?>
<li>
<a href="<?php echo $signUpUrl; ?>" class="btn-signup">
<?php echo in_array($item->id, $subscribedPlanIds) ? Text::_($renewLanguageItem) : Text::_($signUpLanguageItem); ?>
</a>
</li>
<?php
}
if (in_array('upgrade', $actions))
{
if ($language->hasKey('OSM_UPGRADE_PLAN_' . $item->id))
{
$upgradeLanguageItem = 'OSM_UPGRADE_PLAN_' . $item->id;
}
else
{
$upgradeLanguageItem = 'OSM_UPGRADE';
}
if (count($item->upgrade_rules) > 1)
{
$link = Route::_('index.php?option=com_osmembership&view=upgrademembership&to_plan_id=' . $item->id . '&Itemid=' . OSMembershipHelperRoute::findView('upgrademembership', $Itemid));
}
else
{
$upgradeOptionId = $item->upgrade_rules[0]->id;
$link = Route::_('index.php?option=com_osmembership&task=register.process_upgrade_membership&upgrade_option_id=' . $upgradeOptionId . '&Itemid=' . $Itemid);
}
?>
<li>
<a href="<?php echo $link; ?>" class="btn-signup">
<?php echo Text::_($upgradeLanguageItem); ?>
</a>
</li>
<?php
}
}
if ($showDetailsButton)
{
?>
<li>
<a href="<?php echo $url; ?>" class="btn-signup oms-btn-details">
<?php echo Text::_('OSM_DETAILS'); ?>
</a>
</li>
<?php
}
?>
</ul>
</div>
</div>
<?php
if (($i + 1) % $numberColumns == 0)
{
?>
</div>
<?php
}
$i++;
}
if ($i % $numberColumns != 0)
{
echo '</div>' ;
}

View File

@@ -0,0 +1,261 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Uri\Uri;
/**
* Layout variables
*
* @var array $items
* @var MPFInput $input
* @var MPFConfig $config
* @var int $Itemid
* @var int $categoryId
* @var OSMembershipHelperBootstrap $bootstrapHelper
*/
$subscribedPlanIds = OSMembershipHelperSubscription::getSubscribedPlans();
if (empty($params))
{
$params = Factory::getApplication()->getParams();
}
// Background color settings
$badgeBgColor = $params->get('recommended_badge_background_color');
$headerBgColor = $params->get('header_background_color');
$priceBgColor = $params->get('price_background_color');
$recommendedPriceBgColor = $params->get('recommended_plan_price_background_color');
if (isset($input) && $input->getInt('recommended_plan_id'))
{
$recommendedPlanId = $input->getInt('recommended_plan_id');
}
else
{
$recommendedPlanId = (int) $params->get('recommended_campaign_id');
}
$showDetailsButton = $params->get('show_details_button', 0);
if (isset($input) && $input->getInt('number_columns'))
{
$numberColumns = $input->getInt('number_columns');
}
elseif (isset($config->number_columns))
{
$numberColumns = $config->number_columns ;
}
else
{
$numberColumns = 3 ;
}
$numberColumns = min($numberColumns, 4);
if (!isset($categoryId))
{
$categoryId = 0;
}
$span = intval(12 / $numberColumns);
$btnClass = $bootstrapHelper->getClassMapping('btn');
$btnPrimaryClass = $bootstrapHelper->getClassMapping('btn btn-primary');
$imgClass = $bootstrapHelper->getClassMapping('img-polaroid');
$spanClass = $bootstrapHelper->getClassMapping('span' . $span);
$rootUri = Uri::root(true);
$i = 0;
$numberPlans = count($items);
$defaultItemId = $Itemid;
foreach ($items as $item)
{
$Itemid = OSMembershipHelperRoute::getPlanMenuId($item->id, $item->category_id, $defaultItemId);
if ($item->thumb)
{
$imgSrc = $rootUri . '/media/com_osmembership/' . $item->thumb;
}
$url = Route::_('index.php?option=com_osmembership&view=plan&catid=' . $item->category_id . '&id=' . $item->id . '&Itemid=' . $Itemid);
if ($config->use_https)
{
$signUpUrl = Route::_(OSMembershipHelperRoute::getSignupRoute($item->id, $Itemid), false, 1);
}
else
{
$signUpUrl = Route::_(OSMembershipHelperRoute::getSignupRoute($item->id, $Itemid));
}
if (!$item->short_description)
{
$item->short_description = $item->description;
}
if ($item->id == $recommendedPlanId)
{
$recommended = true;
}
else
{
$recommended = false;
}
if ($recommended && $recommendedPriceBgColor)
{
$planPriceBackgroundColor = $recommendedPriceBgColor;
}
elseif ($priceBgColor)
{
$planPriceBackgroundColor = $priceBgColor;
}
else
{
$planPriceBackgroundColor = '';
}
if ($i % $numberColumns == 0)
{
?>
<div class="<?php echo $bootstrapHelper->getClassMapping('row-fluid clearfix'); ?> osm-pricing-table">
<?php
}
?>
<div class="<?php echo $spanClass; ?>">
<div class="osm-plan<?php if ($recommended) echo ' osm-plan-recommended'; ?> osm-plan-<?php echo $item->id; ?>">
<?php
if ($recommended)
{
?>
<p class="plan-recommended"<?php if ($badgeBgColor) echo ' style=" background-color:' . $badgeBgColor . '";'; ?>><?php echo Text::_('OSM_RECOMMENDED'); ?></p>
<?php
}
?>
<div class="osm-plan-header"<?php if ($headerBgColor) echo ' style=" background-color:' . $headerBgColor . '";'; ?>>
<h2 class="osm-plan-title">
<?php echo $item->title; ?>
</h2>
</div>
<div class="osm-plan-price"<?php if ($planPriceBackgroundColor) echo ' style=" background-color:' . $planPriceBackgroundColor . '";'; ?>>
<h2>
<p class="price">
<?php echo OSMembershipHelperHtml::loadCommonLayout('common/tmpl/priceduration.php', ['item' => $item]); ?>
</p>
</h2>
</div>
<div class="osm-plan-short-description">
<?php echo $item->short_description;?>
</div>
<?php
$actions = OSMembershipHelperSubscription::getAllowedActions($item);
if (count($actions) || $showDetailsButton)
{
$language = Factory::getApplication()->getLanguage();
?>
<ul class="osm-signup-container">
<?php
if (in_array('subscribe', $actions))
{
if ($language->hasKey('OSM_SIGNUP_PLAN_' . $item->id))
{
$signUpLanguageItem = 'OSM_SIGNUP_PLAN_' . $item->id;
}
else
{
$signUpLanguageItem = 'OSM_SIGNUP';
}
if ($language->hasKey('OSM_RENEW_PLAN_' . $item->id))
{
$renewLanguageItem = 'OSM_RENEW_PLAN_' . $item->id;
}
else
{
$renewLanguageItem = 'OSM_RENEW';
}
?>
<li>
<a href="<?php echo $signUpUrl; ?>" class="<?php echo $btnPrimaryClass; ?> btn-singup">
<?php echo in_array($item->id, $subscribedPlanIds) ? Text::_($renewLanguageItem) : Text::_($signUpLanguageItem); ?>
</a>
</li>
<?php
}
if (in_array('upgrade', $actions))
{
if ($language->hasKey('OSM_UPGRADE_PLAN_' . $item->id))
{
$upgradeLanguageItem = 'OSM_UPGRADE_PLAN_' . $item->id;
}
else
{
$upgradeLanguageItem = 'OSM_UPGRADE';
}
if (count($item->upgrade_rules) > 1)
{
$link = Route::_('index.php?option=com_osmembership&view=upgrademembership&to_plan_id=' . $item->id . '&Itemid=' . OSMembershipHelperRoute::findView('upgrademembership', $Itemid));
}
else
{
$upgradeOptionId = $item->upgrade_rules[0]->id;
$link = Route::_('index.php?option=com_osmembership&task=register.process_upgrade_membership&upgrade_option_id=' . $upgradeOptionId . '&Itemid=' . $Itemid);
}
?>
<li>
<a href="<?php echo $link; ?>" class="<?php echo $btnPrimaryClass; ?> btn-singup">
<?php echo Text::_($upgradeLanguageItem); ?>
</a>
</li>
<?php
}
if ($showDetailsButton)
{
?>
<li>
<a href="<?php echo $url; ?>" class="<?php echo $btnClass; ?>">
<?php echo Text::_('OSM_DETAILS'); ?>
</a>
</li>
<?php
}
?>
</ul>
<?php
}
?>
</div>
</div>
<?php
if (($i + 1) % $numberColumns == 0)
{
?>
</div>
<?php
}
$i++;
}
if ($i % $numberColumns != 0)
{
echo '</div>' ;
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
?>
<ul class="osm-renew-options">
<?php
$userId = Factory::getApplication()->getIdentity()->id;
$renewOptionCount = 0;
$fieldSuffix = OSMembershipHelper::getFieldSuffix();
foreach ($this->planIds as $planId)
{
$plan = $this->plans[$planId];
$taxRate = 0;
if ($this->config->show_price_including_tax && !$this->config->setup_price_including_tax)
{
$taxRate = OSMembershipHelper::calculateMaxTaxRate($planId);
}
$symbol = $plan->currency_symbol ?: $plan->currency;
$renewOptions = $this->renewOptions[$planId] ?? [];
if (count($renewOptions))
{
foreach ($renewOptions as $renewOption)
{
$checked = '';
if ($renewOptionCount == 0)
{
$checked = ' checked="checked" ';
}
$renewOptionCount++;
$renewOptionLengthText = OSMembershipHelperSubscription::getDurationText($renewOption->renew_option_length, $renewOption->renew_option_length_unit);
$renewOptionText = Text::sprintf('OSM_RENEW_OPTION_TEXT', $plan->title, $renewOptionLengthText, OSMembershipHelper::formatCurrency($renewOption->price * (1 + $taxRate / 100), $this->config, $symbol));
if (strpos($renewOptionText, '[EXPIRED_DATE]'))
{
$expiredDate = OSMembershipHelperSubscription::getPlanExpiredDate($planId);
if ($expiredDate)
{
$expiredDate = HTMLHelper::_('date', $expiredDate, $this->config->date_format);
}
$renewOptionText = str_replace('[EXPIRED_DATE]', $expiredDate, $renewOptionText);
}
?>
<li class="osm-renew-option">
<input type="radio" class="validate[required]<?php echo $this->bootstrapHelper->getFrameworkClass('uk-radio', 1); ?>" id="renew_option_id_<?php echo $renewOptionCount; ?>" name="renew_option_id" value="<?php echo $planId . '|' . $renewOption->id; ?>" <?php echo $checked; ?> />
<label for="renew_option_id_<?php echo $renewOptionCount; ?>"><?php echo $renewOptionText; ?></label>
</li>
<?php
}
}
else
{
$checked = '';
if ($renewOptionCount == 0)
{
$checked = ' checked="checked" ';
}
$renewOptionCount++;
$subscriptionLengthText = OSMembershipHelperSubscription::getDurationText($plan->subscription_length, $plan->subscription_length_unit);
$renewalDiscountRule = OSMembershipHelperSubscription::getRenewalDiscount($userId, $planId);
if ($renewalDiscountRule)
{
if ($renewalDiscountRule->discount_type == 0)
{
$plan->price = round($plan->price * (1 - $renewalDiscountRule->discount_amount / 100), 2);
}
else
{
$plan->price = $plan->price - $renewalDiscountRule->discount_amount;
}
if ($plan->price < 0)
{
$plan->price = 0;
}
}
$renewOptionText = Text::sprintf('OSM_RENEW_OPTION_TEXT', $plan->title, $subscriptionLengthText, OSMembershipHelper::formatCurrency($plan->price * (1 + $taxRate / 100), $this->config, $symbol));
if (strpos($renewOptionText, '[EXPIRED_DATE]'))
{
$expiredDate = OSMembershipHelperSubscription::getPlanExpiredDate($plan->id);
if ($expiredDate)
{
$expiredDate = HTMLHelper::_('date', $expiredDate, $this->config->date_format);
}
$renewOptionText = str_replace('[EXPIRED_DATE]', $expiredDate, $renewOptionText);
}
?>
<li class="osm-renew-option">
<input type="radio" class="validate[required]<?php echo $this->bootstrapHelper->getFrameworkClass('uk-radio', 1); ?>" id="renew_option_id_<?php echo $renewOptionCount; ?>" name="renew_option_id" value="<?php echo $planId;?>" <?php echo $checked; ?>/>
<label for="renew_option_id_<?php echo $renewOptionCount; ?>"><?php echo $renewOptionText; ?></label>
</li>
<?php
}
}
?>
</ul>
<div class="form-actions">
<input type="submit" class="<?php echo $this->bootstrapHelper->getClassMapping('btn btn-primary'); ?>" value="<?php echo Text::_('OSM_PROCESS_RENEW'); ?>"/>
</div>

View File

@@ -0,0 +1,31 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
/**
* Layout variables
*
* @var string $introText
* @var string $msg
* @var string $context
* @var stdClass $row
*/
if (isset($introText))
{
echo '<div class="intro-text">' . $introText . '</div>';
}
?>
<div class="text-info">
<?php echo $msg; ?>
</div>

View File

@@ -0,0 +1,176 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
/**
* Layout variables
*
* @var bool $showPagination
* @var \Joomla\CMS\Pagination\Pagination $pagination
*/
/* @var \Joomla\Database\DatabaseDriver $db */
$db = Factory::getContainer()->get('db');
$query = $db->getQuery(true)
->select('COUNT(*)')
->from('#__osmembership_plugins')
->where('published = 1')
->where('name NOT LIKE "os_offline%"');
$db->setQuery($query);
$hasOnlinePaymentPlugin = $db->loadResult() > 0;
$makePaymentItemid = OSMembershipHelperRoute::getViewRoute('payment', $this->Itemid);
$cols = 5;
$bootstrapHelper = OSMembershipHelperBootstrap::getInstance();
$centerClass = $bootstrapHelper->getClassMapping('center');
$hiddenPhoneClass = $bootstrapHelper->getClassMapping('hidden-phone');
?>
<table class="<?php echo $bootstrapHelper->getClassMapping('table table-striped table-bordered') ?>">
<thead>
<tr>
<th>
<?php echo Text::_('OSM_PLAN') ?>
</th>
<th class="<?php echo $centerClass; ?>">
<?php echo Text::_('OSM_SUBSCRIPTION_DATE') ; ?>
</th>
<th class="<?php echo $centerClass; ?>">
<?php echo Text::_('OSM_ACTIVATE_TIME') ; ?>
</th>
<th style="text-align: right;" class="<?php echo $hiddenPhoneClass; ?>">
<?php echo Text::_('OSM_GROSS_AMOUNT') ; ?>
</th>
<th class="<?php echo $hiddenPhoneClass; ?>">
<?php echo Text::_('OSM_SUBSCRIPTION_STATUS'); ?>
</th>
<?php
if ($this->config->activate_invoice_feature)
{
$cols++ ;
?>
<th class="<?php echo $hiddenPhoneClass . ' ' . $centerClass; ?>">
<?php echo Text::_('OSM_INVOICE_NUMBER') ; ?>
</th>
<?php
}
?>
</tr>
</thead>
<tbody>
<?php
$k = 0 ;
for ($i = 0 , $n = count($this->items) ; $i < $n ; $i++) {
$row = $this->items[$i];
$k = 1 - $k;
$link = Route::_('index.php?option=com_osmembership&view=subscription&id=' . $row->id . '&Itemid=' . $this->Itemid);
$symbol = $row->currency_symbol ?: $row->currency;
?>
<tr>
<td>
<a href="<?php echo $link; ?>"><?php echo $row->plan_title; ?></a>
</td>
<td class="<?php echo $centerClass; ?>">
<?php echo HTMLHelper::_('date', $row->created_date, $this->config->date_format); ?>
</td>
<td class="<?php echo $centerClass; ?>">
<strong><?php echo HTMLHelper::_('date', $row->from_date, $this->config->date_format); ?></strong> <?php echo Text::_('OSM_TO'); ?>
<strong>
<?php
if ($row->lifetime_membership || $row->to_date == '2099-12-31 23:59:59')
{
echo Text::_('OSM_LIFETIME');
}
else
{
echo HTMLHelper::_('date', $row->to_date, $this->config->date_format);
}
?>
</strong>
</td>
<td style="text-align: right;" class="<?php echo $hiddenPhoneClass; ?>">
<?php echo OSMembershipHelper::formatCurrency($row->gross_amount, $this->config, $symbol)?>
</td>
<td class="<?php echo $hiddenPhoneClass; ?>">
<?php
switch ($row->published)
{
case 0 :
echo Text::_('OSM_PENDING');
if ($this->config->enable_subscription_payment && $row->gross_amount > 0 && $hasOnlinePaymentPlugin)
{
?>
<br /><a class="<?php echo $bootstrapHelper->getClassMapping('btn btn-primary'); ?>" href="<?php echo Route::_('index.php?option=com_osmembership&view=payment&transaction_id=' . $row->transaction_id . '&Itemid=' . $makePaymentItemid); ?>"><?php echo Text::_('OSM_MAKE_PAYMENT'); ?></a>
<?php
}
break;
case 1 :
echo Text::_('OSM_ACTIVE');
break;
case 2 :
echo Text::_('OSM_EXPIRED');
break;
case 3 :
echo Text::_('OSM_CANCELLED_PENDING');
break;
case 4 :
echo Text::_('OSM_CANCELLED_REFUNDED');
break;
}
?>
</td>
<?php
if ($this->config->activate_invoice_feature)
{
?>
<td class="<?php echo $hiddenPhoneClass . ' ' . $centerClass; ?>">
<?php
if ($row->invoice_number)
{
?>
<a href="<?php echo Route::_('index.php?option=com_osmembership&task=download_invoice&id=' . $row->id); ?>" title="<?php echo Text::_('OSM_DOWNLOAD'); ?>"><?php echo OSMembershipHelper::formatInvoiceNumber($row, $this->config); ?></a>
<?php
}
?>
</td>
<?php
}
?>
</tr>
<?php
}
?>
</tbody>
<?php
if ($showPagination && ($pagination->total > $pagination->limit))
{
?>
<tfoot>
<tr>
<td colspan="<?php echo $cols; ?>">
<div class="pagination"><?php echo $this->pagination->getListFooter(); ?></div>
</td>
</tr>
</tfoot>
<?php
}
?>
</table>

View File

@@ -0,0 +1,105 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
/**
* Layout variables
*
* @var array $rows
* @var array $fields
*/
$config = OSMembershipHelper::getConfig();
$i = 1;
?>
<p style="padding-bottom: 20px; text-align: center;">
<h1><?php echo Text::_('OSM_SUBSCRIPTIONS_LIST'); ?></h1>
</p>
<table border="1" width="100%" cellspacing="0" cellpadding="2" style="margin-top: 100px;">
<thead>
<tr>
<th width="3%" height="20" style="text-align: center;">
No
</th>
<th height="20" width="8%">
<?php echo Text::_('OSM_FIRSTNAME'); ?>
</th height="20">
<th height="20" width="10%">
<?php echo Text::_('OSM_LASTNAME'); ?>
</th height="20">
<th height="20" width="20%">
<?php echo Text::_('OSM_PLAN'); ?>
</th>
<th height="20" width="17%" style="text-align: center">
<?php echo Text::_('OSM_START_DATE') . ' / ' . Text::_('OSM_END_DATE'); ?>
</th>
<th height="20" width="16%">
<?php echo Text::_('OSM_EMAIL'); ?>
</th>
<th height="20" width="9%" style="text-align: center;">
<?php echo Text::_('OSM_CREATED_DATE'); ?>
</th>
<th width="6%" height="20" style="text-align: right;">
<?php echo Text::_('OSM_GROSS_AMOUNT'); ?>
</th>
<th width="8%" height="20">
<?php echo Text::_('OSM_SUBSCRIPTION_STATUS'); ?>
</th>
<th width="3%" height="20" style="text-align: center;">
<?php echo Text::_('OSM_ID'); ?>
</th>
</tr>
</thead>
<tbody>
<?php
foreach ($rows as $row)
{
?>
<tr>
<td width="3%" style="text-align: center;"><?php echo $i++; ?></td>
<td width="8%"><?php echo $row->first_name; ?></td>
<td width="10%"><?php echo $row->last_name; ?></td>
<td width="20%;"><?php echo $row->plan; ?></td>
<td width="17%" style="text-align: center"><?php echo $row->from_date . ' / ' . $row->to_date; ?></td>
<td width="16%"><?php echo $row->email; ?></td>
<td width="9%" style="text-align: center;"><?php echo $row->created_date; ?></td>
<td width="6%" style="text-align: right;"><?php echo $row->amount; ?></td>
<th width="8%" height="20">
<?php
switch ($row->published)
{
case 0:
echo Text::_('OSM_PENDING');
break;
case 1:
echo Text::_('OSM_ACTIVE');
break;
case 2:
echo Text::_('OSM_EXPIRED');
break;
case 3 :
echo Text::_('OSM_CANCELLED_PENDING');
break ;
case 4 :
echo Text::_('OSM_CANCELLED_REFUNDED');
break ;
}
?>
</th>
<td width="3%" style="text-align: center;"><?php echo $row->id; ?></td>
</tr>
<?php
}
?>
</tbody>
</table>

View File

@@ -0,0 +1,48 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
use Joomla\CMS\Language\Text;
?>
<ul class="osm-upgrade-options">
<?php
$upgradeOptionCount = 0;
foreach ($this->upgradeRules as $rule)
{
$checked = '';
if ($upgradeOptionCount == 0)
{
$checked = ' checked="checked" ';
}
$upgradeOptionCount++;
$upgradeToPlan = $this->plans[$rule->to_plan_id];
$symbol = $upgradeToPlan->currency_symbol ?: $upgradeToPlan->currency;
$taxRate = 0;
if ($this->config->show_price_including_tax && !$this->config->setup_price_including_tax)
{
$taxRate = OSMembershipHelper::calculateMaxTaxRate($rule->to_plan_id);
}
?>
<li class="osm-upgrade-option">
<input type="radio" class="validate[required]<?php echo $this->bootstrapHelper->getFrameworkClass('uk-radio', 1);?>" id="upgrade_option_id_<?php echo $upgradeOptionCount; ?>" name="upgrade_option_id" value="<?php echo $rule->id; ?>"<?php echo $checked; ?> />
<label for="upgrade_option_id_<?php echo $upgradeOptionCount; ?>"><?php Text::printf('OSM_UPGRADE_OPTION_TEXT', $this->plans[$rule->from_plan_id]->title, $upgradeToPlan->title, OSMembershipHelper::formatCurrency($rule->price * (1 + $taxRate / 100), $this->config, $symbol)); ?></label>
</li>
<?php
}
?>
</ul>

View File

@@ -0,0 +1,118 @@
<!--
* Copyright (C) 2025 Moko Consulting <jmiller@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!--FILE INFORMATION
* DEFGROUP: Joomla.Site
* INGROUP: Templates.Moko-Cassiopeia
* FILE: index.html
* BRIEF: Security redirect page to block folder access and forward to site root.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Redirecting…</title>
<!-- Search engines: do not index this placeholder redirect page -->
<meta name="robots" content="noindex, nofollow, noarchive" />
<!-- Instant redirect fallback even if JavaScript is disabled -->
<meta http-equiv="refresh" content="0; url=/" />
<!-- Canonical root reference -->
<link rel="canonical" href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
/**
* @defgroup Dolibarr
* @file index.html (embedded script)
* @version 1.0.0
* @brief Security redirect logic. Replaces the current history entry with the site root.
* @details This script computes the absolute root URL using `location.origin` and
* forwards the user immediately. It prevents leaving the protected folder
* in the browser history by default.
*
* @section VARIABLES
* @var {Object} opts Configuration options for the redirect behavior.
* @var {string} opts.fallbackPath Path used when `location.origin` cannot be determined.
* @var {number} opts.delayMs Optional delay in milliseconds before redirecting.
* @var {"replace"|"assign"} opts.behavior Navigation method used for the redirect.
*
* @section OPTIONS
* - opts.fallbackPath: default "/" (root path)
* - opts.delayMs: default 0 (immediate)
* - opts.behavior: one of
* * "replace" — calls `location.replace(url)`; does not keep the folder page in history.
* * "assign" — calls `location.assign(url)`; keeps an extra history entry.
*/
(function redirectToRoot() {
// Configuration object with safe defaults.
var opts = {
fallbackPath: "/", // string: fallback destination if origin is unavailable
delayMs: 0, // number: delay before redirect in ms (0 = immediate)
behavior: "replace" // enum: "replace" | "assign"
};
// Determine absolute origin in all mainstream browsers.
var origin = (typeof location.origin === "string" && location.origin)
|| (location.protocol + "//" + location.host);
// Final destination: absolute root of the current site, or fallback path.
var destination = origin ? origin + "/" : opts.fallbackPath;
function go() {
if (opts.behavior === "assign") {
location.assign(destination);
} else {
location.replace(destination);
}
}
// Execute redirect, optionally after a short delay.
if (opts.delayMs > 0) {
setTimeout(go, opts.delayMs);
} else {
go();
}
})();
</script>
<!--
Secondary meta-refresh for no-JS environments is already set above.
Some very old crawlers may ignore JS; the meta refresh ensures coverage.
-->
<noscript>
<!-- Extra defense-in-depth: if JS is disabled, meta refresh (above) handles redirect. -->
<style>
html, body { height:100%; }
body { display:flex; align-items:center; justify-content:center; margin:0; font: 16px/1.4 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
.msg { opacity: .75; text-align: center; }
</style>
</noscript>
</head>
<body>
<div class="msg">Redirecting to the site root… If you are not redirected, <a href="/">click here</a>.</div>
</body>
</html>

View File

@@ -0,0 +1,141 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die ;
use Joomla\CMS\Editor\Editor;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Multilanguage;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
use Joomla\CMS\Toolbar\Toolbar;
HTMLHelper::_('bootstrap.tooltip', '.hasTooltip', ['html' => true, 'sanitize' => false]);
$config = OSMembershipHelper::getConfig();
$editor = Editor::getInstance($config->get('editor') ?: Factory::getApplication()->get('editor'));
$translatable = Multilanguage::isEnabled() && count($this->languages);
$bootstrapHelper = OSMembershipHelperBootstrap::getInstance();
$rowFluid = $bootstrapHelper->getClassMapping('row-fluid');
$span8 = $bootstrapHelper->getClassMapping('span7');
$span4 = $bootstrapHelper->getClassMapping('span5');
HTMLHelper::_('formbehavior.chosen', '.advSelect');
Factory::getApplication()
->getDocument()
->getWebAssetManager()
->useScript('core')
->useScript('showon')
->registerAndUseScript('com_osmembership.site-mplan-default', 'media/com_osmembership/js/site-mplan-default.min.js');
$keys = ['OSM_ENTER_PLAN_TITLE', 'OSM_ENTER_SUBSCRIPTION_LENGTH', 'OSM_PRICE_REQUIRED', 'OSM_INVALID_SUBSCRIPTION_LENGTH'];
OSMembershipHelperHtml::addJSStrings($keys);
?>
<div id="osm-add-edit-plan" class="osm-container">
<h1 class="osm-page-title"><?php echo $this->item->id > 0 ? Text::_('OSM_EDIT_PLAN') : Text::_('OSM_ADD_PLAN'); ?></h1>
<div class="btn-toolbar" id="btn-toolbar">
<?php echo Toolbar::getInstance('toolbar')->render(); ?>
</div>
<form action="<?php echo Route::_('index.php?option=com_osmembership&view=mplan&Itemid=' . $this->Itemid, false); ?>" method="post" name="adminForm" id="adminForm" enctype="multipart/form-data" class="form form-horizontal">
<?php
echo HTMLHelper::_( 'uitab.startTabSet', 'plan', ['active' => 'basic-information-page', 'recall' => true]);
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'basic-information-page', Text::_('OSM_BASIC_INFORMATION'));
echo $this->loadTemplate('general', ['editor' => $editor]);
echo HTMLHelper::_( 'uitab.endTab');
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'recurring-settings-page', Text::_('OSM_RECURRING_SETTINGS'));
echo $this->loadTemplate('recurring_settings');
echo HTMLHelper::_( 'uitab.endTab');
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'renew-options-page', Text::_('OSM_RENEW_OPTIONS'));
echo $this->loadTemplate('renew_options');
echo HTMLHelper::_( 'uitab.endTab');
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'upgrade-options-page', Text::_('OSM_UPGRADE_OPTIONS'));
echo $this->loadTemplate('upgrade_options');
echo HTMLHelper::_( 'uitab.endTab');
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'renewal-discounts-page', Text::_('OSM_EARLY_RENEWAL_DISCOUNTS'));
echo $this->loadTemplate('renewal_discounts');
echo HTMLHelper::_( 'uitab.endTab');
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'reminders-settings-page', Text::_('OSM_REMINDERS_SETTINGS'));
echo $this->loadTemplate('reminders_settings');
echo HTMLHelper::_( 'uitab.endTab');
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'group-membership-settings-page', Text::_('OSM_GROUP_MEMBERSHIP'));
echo $this->loadTemplate('group_membership');
echo HTMLHelper::_( 'uitab.endTab');
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'advanced-settings-page', Text::_('OSM_ADVANCED_SETTINGS'));
echo $this->loadTemplate('advanced_settings');
echo HTMLHelper::_( 'uitab.endTab');
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'metadata-page', Text::_('OSM_META_DATA'));
echo $this->loadTemplate('metadata');
echo HTMLHelper::_( 'uitab.endTab');
if ($this->config->activate_member_card_feature)
{
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'member-card-page', Text::_('OSM_MEMBER_CARD_SETTINGS'));
echo $this->loadTemplate('member_card', ['editor' => $editor]);
echo HTMLHelper::_( 'uitab.endTab');
}
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'messages-page', Text::_('OSM_MESSAGES'));
echo $this->loadTemplate('messages', ['editor' => $editor]);
echo HTMLHelper::_( 'uitab.endTab');
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'reminder-messages-page', Text::_('OSM_REMINDER_MESSAGES'));
echo $this->loadTemplate('reminder_messages', ['editor' => $editor]);
echo HTMLHelper::_( 'uitab.endTab');
if ($translatable)
{
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'translation-page', Text::_('OSM_TRANSLATION'));
echo $this->loadTemplate('translation', ['editor' => $editor]);
echo HTMLHelper::_( 'uitab.endTab');
}
if (count($this->plugins))
{
$count = 0 ;
foreach ($this->plugins as $plugin)
{
if (is_array($plugin) && array_key_exists('title', $plugin) && array_key_exists('form', $plugin))
{
$count++ ;
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'tab_' . $count, Text::_($plugin['title']));
echo $plugin['form'];
echo HTMLHelper::_( 'uitab.endTab');
}
}
}
// Add support for custom settings layout
if (file_exists(__DIR__ . '/default_custom_settings.php'))
{
echo HTMLHelper::_( 'uitab.addTab', 'plan', 'custom-settings-page', Text::_('OSM_CUSTOM_SETTINGS'));
echo $this->loadTemplate('custom_settings', ['editor' => $editor]);
echo HTMLHelper::_( 'uitab.endTab');
}
echo HTMLHelper::_( 'uitab.endTabSet');
?>
<div class="clearfix"></div>
<?php echo HTMLHelper::_('form.token'); ?>
<input type="hidden" name="id" value="<?php echo (int) $this->item->id; ?>"/>
<input type="hidden" name="task" value="apply" />
<input type="hidden" id="recurring" name="recurring" value="<?php echo (int) $this->item->recurring_subscription;?>" />
</form>
</div>

View File

@@ -0,0 +1,173 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
$bootstrapHelper = OSMembershipHelperBootstrap::getInstance();
$rowFluidClasss = $bootstrapHelper->getClassMapping('row-fluid');
$controlGroupClass = $bootstrapHelper->getClassMapping('control-group');
$controlLabelClass = $bootstrapHelper->getClassMapping('control-label');
$controlsClass = $bootstrapHelper->getClassMapping('controls');
?>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('setup_fee', Text::_('OSM_SETUP_FEE'), Text::_('OSM_SETUP_FEE_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<input type="number" class="form-control input-small" name="setup_fee" id="setup_fee" value="<?php echo $this->item->setup_fee; ?>" step="0.01" />
</div>
</div>
<?php
if ($this->item->id && !$this->item->recurring_subscription)
{
?>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('subscription_start_date_option', Text::_('OSM_SUBSCRIPTION_START_DATE_OPTION'), Text::_('OSM_SUBSCRIPTION_START_DATE_OPTION_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $this->lists['subscription_start_date_option'];?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>" data-showon='<?php echo OSMembershipHelperHtml::renderShowon(['subscription_start_date_option' => '1']); ?>'>
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('subscription_start_date', Text::_('OSM_PLAN_SUBSCRIPTION_START_DATE'), Text::_('OSM_PLAN_SUBSCRIPTION_START_DATE_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo HTMLHelper::_('calendar', $this->planParams->get('subscription_start_date'), 'subscription_start_date', 'subscription_start_date', '%Y-%m-%d %H:%M:%S') ; ?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>" data-showon='<?php echo OSMembershipHelperHtml::renderShowon(['subscription_start_date_option' => '2']); ?>'>
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('subscription_start_date_field', Text::_('OSM_SUBSCRIPTION_START_DATE_FIELD'), Text::_('OSM_SUBSCRIPTION_START_DATE_FIELD_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $this->lists['subscription_start_date_field'];?>
</div>
</div>
<?php
}
?>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('free_plan_subscription_status', Text::_('OSM_FREE_PLAN_STATUS'), Text::_('OSM_FREE_PLAN_STATUS_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $this->lists['free_plan_subscription_status'];?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('login_redirect_menu_id', Text::_('OSM_LOGIN_REDIRECT'), Text::_('OSM_LOGIN_REDIRECT_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $this->lists['login_redirect_menu_id']; ?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('number_fields_per_row', Text::_('OSM_NUMBER_FIELDS_PER_ROW'), Text::_('OSM_NUMBER_FIELDS_PER_ROW_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $this->lists['number_fields_per_row']; ?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('payment_methods', Text::_('OSM_PAYMENT_METHODS'), Text::_('OSM_PAYMENT_METHODS_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $this->lists['payment_methods'];?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('currency_code', Text::_('OSM_CURRENCY'), Text::_('OSM_CURRENCY_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $this->lists['currency'];?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('currency_symbol', Text::_('OSM_CURRENCY_SYMBOL'), Text::_('OSM_CURRENCY_SYMBOL_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<input type="text" class="form-control input-small" name="currency_symbol" id="currency_symbol" value="<?php echo $this->item->currency_symbol; ?>" />
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_SUBSCRIPTION_COMPLETE_URL'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<input type="url" class="form-control input-xxlarge" name="subscription_complete_url" value="<?php echo $this->item->subscription_complete_url; ?>" />
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_OFFLINE_PAYMENT_SUBSCRIPTION_COMPLETE_URL'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<input type="url" class="form-control input-xxlarge" name="offline_payment_subscription_complete_url" value="<?php echo $this->item->offline_payment_subscription_complete_url; ?>" />
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('notification_emails', Text::_('OSM_NOTIFICATION_EMAILS'), Text::_('OSM_NOTIFICATION_EMAILS_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<input type="text" class="form-control input-xxlarge" name="notification_emails" value="<?php echo $this->item->notification_emails; ?>" />
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('paypal_email', Text::_('OSM_PAYPAL_EMAIL'), Text::_('OSM_PAYPAL_EMAIL_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<input type="email" class="form-control input-xxlarge" name="paypal_email" value="<?php echo $this->item->paypal_email; ?>" />
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_PUBLISH_UP'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo HTMLHelper::_('calendar', $this->item->publish_up, 'publish_up', 'publish_up', $this->datePickerFormat . ' %H:%M:%S', ['class' => 'input-medium']); ?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_PUBLISH_DOWN'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo HTMLHelper::_('calendar', $this->item->publish_down, 'publish_down', 'publish_down', $this->datePickerFormat . ' %H:%M:%S', ['class' => 'input-medium']); ?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_TERMS_AND_CONDITIONS_ARTICLE') ; ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo OSMembershipHelperHtml::getArticleInput($this->item->terms_and_conditions_article_id, 'terms_and_conditions_article_id'); ?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('conversion_tracking_code', Text::_('OSM_CONVERSION_TRACKING_CODE'), Text::_('OSM_CONVERSION_TRACKING_CODE_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<textarea name="conversion_tracking_code" class="form-control input-large" rows="10"><?php echo $this->item->conversion_tracking_code;?></textarea>
</div>
</div>

View File

@@ -0,0 +1,166 @@
<?php
/**
* @package Joomla
* @subpackage Membership Pro
* @author Tuan Pham Ngoc
* @copyright Copyright (C) 2012 - 2025 Ossolution Team
* @license GNU/GPL, see LICENSE.php
*/
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
$bootstrapHelper = OSMembershipHelperBootstrap::getInstance();
$rowFluidClasss = $bootstrapHelper->getClassMapping('row-fluid');
$controlGroupClass = $bootstrapHelper->getClassMapping('control-group');
$controlLabelClass = $bootstrapHelper->getClassMapping('control-label');
$controlsClass = $bootstrapHelper->getClassMapping('controls');
?>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_TITLE'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<input class="form-control input-xxlarge" type="text" name="title" id="title" maxlength="250" value="<?php echo $this->item->title;?>" />
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_ALIAS'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<input class="form-control input-xxlarge" type="text" name="alias" id="alias" maxlength="250" value="<?php echo $this->item->alias;?>" />
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_CATEGORY'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $this->lists['category_id']; ?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_PRICE'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<input class="form-control" type="number" name="price" id="price" maxlength="250" value="<?php echo $this->item->price;?>" step="0.01" />
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_SUBSCRIPTION_LENGTH'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<input class="form-control input-small d-inline-block" type="number" min="1" name="subscription_length" id="subscription_length" maxlength="250" value="<?php echo $this->item->subscription_length;?>" /><?php echo $this->lists['subscription_length_unit']; ?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_EXPIRED_DATE'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo HTMLHelper::_('calendar', $this->item->expired_date, 'expired_date', 'expired_date', $this->datePickerFormat) ; ?>
</div>
</div>
<?php
if ((int)$this->item->expired_date)
{
?>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_PRORATED_SIGNUP_COST');?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $this->lists['prorated_signup_cost'];?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo OSMembershipHelperHtml::getFieldLabel('grace_period', Text::_('OSM_OVERLAP_PERIOD'), Text::_('OSM_OVERLAP_PERIOD_EXPLAIN')); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<input class="input-small form-control" type="number" name="grace_period" id="grace_period" maxlength="250" value="<?php echo $this->item->grace_period;?>" /><?php echo ' ' . Text::_('OSM_DAYS'); ?>
</div>
</div>
<?php
}
?>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_LIFETIME_MEMBERSHIP');?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $this->lists['lifetime_membership'];?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_THUMB'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<input type="file" class="form-control" name="thumb_image" size="60" />
<?php
if ($this->item->thumb)
{
?>
<img src="<?php echo Uri::root() . 'media/com_osmembership/' . $this->item->thumb; ?>" class="img_preview" />
<input type="checkbox" name="del_thumb" value="1" /><?php echo Text::_('OSM_DELETE_CURRENT_THUMB'); ?>
<?php
}
?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_ENABLE_RENEWAL'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $this->lists['enable_renewal']; ?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_ACCESS'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $this->lists['access']; ?>
</div>
</div>
<?php
if (isset($this->lists['published']))
{
?>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_PUBLISHED'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $this->lists['published']; ?>
</div>
</div>
<?php
}
?>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_SHORT_DESCRIPTION'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $editor->display('short_description', $this->item->short_description, '100%', '250', '75', '10') ; ?>
</div>
</div>
<div class="<?php echo $controlGroupClass; ?>">
<div class="<?php echo $controlLabelClass; ?>">
<?php echo Text::_('OSM_DESCRIPTION'); ?>
</div>
<div class="<?php echo $controlsClass; ?>">
<?php echo $editor->display('description', $this->item->description, '100%', '250', '75', '10') ; ?>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More