Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d9ffaa942 | ||
|
|
e0e6e28dcc | ||
|
|
616b23ce77 | ||
|
|
b38be8366c | ||
|
|
c2bcd7be22 | ||
|
|
6daa0a6f49 | ||
|
|
73450a17c1 | ||
|
|
8ebfcb6061 | ||
|
|
b6896c60e5 | ||
|
|
d733e26f91 | ||
|
|
4e369d887b | ||
|
|
e8e3c97ccd | ||
|
|
8ebf11d7e4 | ||
|
|
3d84e61ed1 | ||
|
|
0137877439 | ||
|
|
46c653eb21 | ||
|
|
2d61f9bab6 | ||
|
|
e9d2ec64bb | ||
|
|
8a0637eddc | ||
|
|
d84a2989d4 | ||
|
|
6bdd5faa7a | ||
|
|
b495fc9ab0 | ||
|
|
2c0e62bbae |
@@ -1,7 +1,3 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
# Build outputs
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -15,12 +11,14 @@ Thumbs.db
|
||||
|
||||
# Cache & Storage
|
||||
.cache/
|
||||
.sass-cache/
|
||||
admin/storage/cache/
|
||||
admin/storage/geoip/
|
||||
admin/storage/stats.json
|
||||
var/
|
||||
|
||||
# Runtime-compiled theme assets (generated by scssphp, read-only)
|
||||
themes/*/assets/css_compiled/
|
||||
|
||||
# Runtime-compiled theme assets
|
||||
public/themes/
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# Disable directory listing globally
|
||||
Options -Indexes
|
||||
|
||||
# Security - Block access to entire application
|
||||
<Files ~ "^\.">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</Files>
|
||||
|
||||
<FilesMatch "\.(php|ini|log|conf|config|md|map)$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# Block access to backup files
|
||||
<FilesMatch "\.(backup|bak|old|orig|swp|save)$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
|
||||
# Block access to all application files
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
|
||||
# Directory protection - Block all access
|
||||
<Directory />
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</Directory>
|
||||
|
||||
# Only allow access to public directory
|
||||
<Directory "public">
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# Set default directory to public
|
||||
DirectoryIndex public/index.php
|
||||
|
||||
# Redirect root to public directory
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# Redirect root to public
|
||||
RewriteRule ^$ public/ [L]
|
||||
|
||||
# Redirect all other requests to public
|
||||
RewriteCond %{REQUEST_URI} !^/public/
|
||||
RewriteRule ^(.*)$ public/$1 [L]
|
||||
</IfModule>
|
||||
@@ -1,115 +0,0 @@
|
||||
# Agent Instructions for CodePress CMS
|
||||
|
||||
## AI Model
|
||||
- **Huidig model**: `claude-opus-4-6` (OpenCode / `opencode/claude-opus-4-6`)
|
||||
- Sessie gestart: 16 feb 2026
|
||||
|
||||
## Build & Run
|
||||
- **Run Server**: `php -S localhost:8080 cms/router.php` (router nodig voor clean URLs)
|
||||
- **Lint PHP**: `find . -name "*.php" -not -path "./vendor/*" -exec php -l {} \;`
|
||||
- **Dependencies**: Composer vereist voor CommonMark, Twig en scssphp. Geen NPM.
|
||||
- **Admin Console**: Toegankelijk op `/admin.php` (standaard login: `admin` / `admin`)
|
||||
|
||||
## Project Structuur
|
||||
```
|
||||
codepress/
|
||||
├── cms/ # Core CMS engine
|
||||
│ ├── core/
|
||||
│ │ ├── class/
|
||||
│ │ │ ├── CodePressCMS.php # Hoofd CMS class
|
||||
│ │ │ ├── ThemeManager.php # Thema-resolver + Twig render + SCSS compile
|
||||
│ │ │ └── Logger.php # Logging systeem
|
||||
│ │ ├── plugin/
|
||||
│ │ │ ├── PluginManager.php # Plugin loader
|
||||
│ │ │ └── CMSAPI.php # API voor plugins
|
||||
│ │ ├── config.php # Config loader (leest config.json)
|
||||
│ │ └── index.php # Bootstrap (autoloader, requires)
|
||||
│ └── router.php # PHP dev server router (serveert ook /themes/)
|
||||
├── language/ # Taalbestanden (nl/, en/, de/ — elk met site.php + admin.php)
|
||||
├── themes/ # Dynamische thema's (volledig zelfstandig)
|
||||
│ ├── default/ # Standaard thema
|
||||
│ │ ├── theme.json # { title, default_layout, layout→.twig mapping, kleuren }
|
||||
│ │ ├── base.twig # Hoofd layout (head, header, nav, footer)
|
||||
│ │ ├── full_content.twig # Layout: volledige breedte
|
||||
│ │ ├── left_sidebar.twig # Layout: sidebar links
|
||||
│ │ ├── right_sidebar.twig # Layout: sidebar rechts
|
||||
│ │ ├── custom1.twig # Layout: custom
|
||||
│ │ ├── guide.twig # Layout: handleiding
|
||||
│ │ ├── partials/ # header.twig, navigation.twig, footer.twig
|
||||
│ │ ├── assets/scss/theme.scss # SCSS bron (runtime gecompileerd)
|
||||
│ │ └── assets/js/ # app.js, bootstrap.bundle.min.js
|
||||
│ ├── demo/ # Demo thema (zelfde structuur, andere look)
|
||||
├── admin/ # Admin paneel
|
||||
│ ├── config/
|
||||
│ │ ├── app.php # Admin app configuratie
|
||||
│ │ ├── admin.json # Gebruikers & security (file-based, gitignored)
|
||||
│ │ └── admin.json.example # Voorbeeld met placeholder-wachtwoord
|
||||
│ ├── src/
|
||||
│ │ └── AdminAuth.php # Authenticatie (sessies, bcrypt, CSRF, lockout)
|
||||
│ ├── theme/default/views/ # Twig templates
|
||||
│ │ ├── login.twig # Login pagina
|
||||
│ │ ├── layouts/admin.twig # Admin layout met sidebar
|
||||
│ │ └── pages/ # dashboard, content, content-edit, config, plugins, theme, users, statistics, logs, security, update, guide, media, etc.
|
||||
│ └── storage/logs/ # Admin logs (gedeeld met front-end)
|
||||
├── cli/ # CLI scripts & tests
|
||||
│ └── test/
|
||||
│ ├── accessibility.sh # WCAG 2.1 AA test suite
|
||||
│ ├── enhanced-suite.sh # Enhanced test suite
|
||||
│ ├── functional/ # Functionele testen
|
||||
│ └── pentest/ # Penetratietesten
|
||||
├── plugins/ # CMS plugins
|
||||
│ ├── HTMLBlock/
|
||||
│ └── Navigation/
|
||||
├── public/ # Web root
|
||||
│ ├── assets/css/js/
|
||||
│ ├── index.php # Website entry point
|
||||
│ └── admin.php # Admin entry point + router
|
||||
├── content/ # Content bestanden
|
||||
├── guide/ # Handleidingen (nl/en)
|
||||
├── docs/ # Documentatie
|
||||
├── config.json # Site configuratie
|
||||
└── AGENTS.md # Dit bestand
|
||||
```
|
||||
|
||||
## Code Style & Conventions
|
||||
- **PHP Standards**: Follow PSR-12. Use 4 spaces for indentation.
|
||||
- **Naming**: Classes `PascalCase` (e.g., `CodePressCMS`), methods `camelCase` (e.g., `renderMenu`), variables `camelCase`, config keys `snake_case`.
|
||||
- **Architecture**:
|
||||
- Core CMS logic in `cms/core/class/CodePressCMS.php`
|
||||
- Bootstrap/requires in `cms/core/index.php`
|
||||
- Configuration loaded from `config.json` via `cms/core/config.php`
|
||||
- Public website entry point: `public/index.php`
|
||||
- Admin entry point + routing: `public/admin.php`
|
||||
- Admin authenticatie: `admin/src/AdminAuth.php`
|
||||
- **Content**: Stored in `content/`. Supports `.md` (Markdown), `.php` (Dynamic), `.html` (Static).
|
||||
- **Templating**: Twig templates in `themes/<naam>/`. `ThemeManager` rendert via Twig en compileert `assets/scss/theme.scss` runtime naar `public/themes/<naam>/theme.css`. Layout gekozen via frontmatter `layout:` key; onbekende layouts vallen terug op `default_layout` in `theme.json`.
|
||||
- **Navigation**: Auto-generated from directory structure. Folders require an index file to be clickable in breadcrumbs.
|
||||
- **Security**:
|
||||
- Always use `htmlspecialchars()` for outputting user/content data
|
||||
- Use `realpath()` + prefix-check for path traversal prevention
|
||||
- Admin forms require CSRF tokens via `AdminAuth::verifyCsrf()`
|
||||
- Passwords stored as bcrypt hashes in `admin.json`
|
||||
- **Git**: `main` is the clean CMS core. `development` is de actieve development branch. `e.noorlander` bevat persoonlijke content. Niet mixen.
|
||||
|
||||
## Admin Console
|
||||
- **File-based**: Geen database. Gebruikers opgeslagen in `admin/config/admin.json`
|
||||
- **Routing**: Via `?route=` parameter in `public/admin.php`
|
||||
- **Routes**: `login`, `logout`, `dashboard`, `content`, `content-edit`, `content-new`, `content-delete`, `content-dir-create`, `content-dir-rename`, `content-dir-delete`, `content-move`, `config`, `plugins`, `plugins-new`, `plugins-edit`, `plugins-config`, `plugins-toggle`, `plugins-delete`, `theme`, `theme-new`, `users`, `security`, `statistics`, `logs`, `guide`, `media`, `update`
|
||||
- **Auth**: Session-based. `AdminAuth` class handelt login, logout, CSRF, brute-force lockout af
|
||||
- **Templates**: Twig templates in `admin/theme/default/views/`. Layout in `layouts/admin.twig`
|
||||
|
||||
## Important: Title vs File/Directory Name Logic
|
||||
- **CRITICAL**: When user asks for "title" corrections, they usually mean **FILE/DIRECTORY NAME WITHOUT LANGUAGE PREFIX AND EXTENSIONS**, not the HTML title from content!
|
||||
- **Examples**:
|
||||
- `nl.test.md` → display as "Test" (not content title)
|
||||
- `nl.test/` directory → display as "Test" (not H1 content)
|
||||
- `en.php-testen` → display as "Php Testen" (not "ICT")
|
||||
- **Method**: Use `formatDisplayName()` to process file/directory names correctly
|
||||
- **Priority**: Directory names take precedence over file names when both exist
|
||||
- **Language prefixes**: Dynamisch verwijderd op basis van beschikbare talen via `getAvailableLanguages()`
|
||||
|
||||
## Bekende aandachtspunten
|
||||
- LSP errors over "Undefined function" in PHP files zijn vals-positief (standaard PHP functies worden niet herkend door de LSP). Negeer deze.
|
||||
- Zie `TODO.md` voor alle openstaande verbeteringen en nieuwe features.
|
||||
- `vendor/` map bevat Composer dependencies (CommonMark, Twig, scssphp, GeoIP2). Niet handmatig wijzigen.
|
||||
- `admin/config/admin.json` bevat wachtwoord-hashes. Niet committen met echte productie-wachtwoorden.
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
A lightweight, file-based content management system built with PHP (≥8.0).
|
||||
|
||||
**Version:** 2.5.1 | **License:** AGPL v3 / Commercial
|
||||
**Version:** 2.6.1 | **License:** AGPL v3 / Commercial
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 📝 **Multi-format Content** - Markdown, PHP and HTML files
|
||||
- 🧭 **Dynamic Navigation** - Automatic menu generation
|
||||
- 🌍 **Multi-language** - NL/EN/DE/FR support
|
||||
- 🌍 **Multi-language** - NL/EN/DE support
|
||||
- 🔍 **Search** - Full-text search
|
||||
- 📱 **Responsive** - Bootstrap 5 themes
|
||||
- 🔒 **Security** - 100/100 pentest score
|
||||
@@ -27,13 +27,141 @@ A lightweight, file-based content management system built with PHP (≥8.0).
|
||||
# Install dependencies
|
||||
composer install
|
||||
|
||||
# Start server with router for clean URLs
|
||||
# Start server with router for clean URLs (local only)
|
||||
php -S localhost:8080 cms/router.php
|
||||
```
|
||||
|
||||
**Website:** `http://localhost:8080`
|
||||
**Admin:** `http://localhost:8080/admin` (login: `admin` / `admin`)
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Requirements
|
||||
|
||||
- **PHP** ≥ 8.0 with extensions: `json`, `mbstring`
|
||||
- **Composer** (PHP dependency manager)
|
||||
- Web server: **Apache 2.4+** with `mod_rewrite` or **Nginx** with PHP-FPM
|
||||
- Optional: `opcache` (recommended for performance), `git` (for content versioning), `zip` extension (for ZIP backup/restore)
|
||||
|
||||
### Step 1 — Code and dependencies
|
||||
|
||||
```bash
|
||||
git clone <repository-url> codepress
|
||||
cd codepress
|
||||
composer install
|
||||
```
|
||||
|
||||
### Step 2 — Configuration
|
||||
|
||||
```bash
|
||||
cp config.json.example config.json
|
||||
cp admin/config/admin.json.example admin/config/admin.json
|
||||
```
|
||||
|
||||
Edit `config.json` with your site title, language and plugins. Change the admin password in `admin/config/admin.json` (default `admin`/`admin`).
|
||||
|
||||
### Step 3a — Apache 2.4+
|
||||
|
||||
The webroot is the `public/` directory. Example vhost (`/etc/apache2/sites-available/codepress.conf`):
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName example.com
|
||||
DocumentRoot /var/www/codepress/public
|
||||
|
||||
<Directory /var/www/codepress/public>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/codepress_error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/codepress_access.log combined
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
Required Apache modules:
|
||||
|
||||
```bash
|
||||
sudo a2enmod rewrite headers
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
- `mod_rewrite` — for clean URLs (`/nl/page`) and asset-serving
|
||||
- `mod_headers` — for security headers
|
||||
- `AllowOverride All` — so the `.htaccess` in `public/` is applied
|
||||
|
||||
### Step 3b — Nginx
|
||||
|
||||
Example server block (`/etc/nginx/sites-available/codepress`):
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
root /var/www/codepress/public;
|
||||
index index.php;
|
||||
|
||||
# Clean URLs: language-prefixed pages
|
||||
location ~ ^/(nl|en|de)(/(.+))?$ {
|
||||
try_files $uri /index.php?lang=$1&page=$2;
|
||||
}
|
||||
|
||||
# Admin routes
|
||||
location /admin {
|
||||
try_files $uri /admin.php?$args;
|
||||
}
|
||||
|
||||
# Asset-serving via asset.php (themes/plugins/admin outside webroot)
|
||||
location ~ ^/(themes|plugins)/([^/]+)/assets/(.+)$ {
|
||||
try_files $uri /asset.php;
|
||||
}
|
||||
location ~ ^/admin/assets/(.+)$ {
|
||||
try_files $uri /asset.php;
|
||||
}
|
||||
|
||||
# PHP via FPM
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/run/php/php8.0-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
# Security: block access to sensitive directories
|
||||
location ~ ^/(content|cms|admin/src|admin/config|admin/storage|var|vendor)/ {
|
||||
deny all;
|
||||
return 403;
|
||||
}
|
||||
|
||||
location ~ /\.(git|htaccess) {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Nginx does not use `.htaccess`. Security headers must be set in the Nginx config:
|
||||
|
||||
```nginx
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header Referrer-Policy strict-origin-when-cross-origin;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';";
|
||||
```
|
||||
|
||||
### Step 4 — Directory permissions
|
||||
|
||||
Make sure the web server has write access to the runtime directories:
|
||||
|
||||
```bash
|
||||
chown -R www-data:www-data var/ admin/storage/ content/
|
||||
chmod -R 755 .
|
||||
```
|
||||
|
||||
### Step 5 — Test
|
||||
|
||||
Open the website in your browser. With an empty content directory you'll see a welcome page. The admin console is available at `/admin` (login `admin`/`admin`).
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
See **[guide/](guide/)** for extensive documentation per role:
|
||||
@@ -68,6 +196,7 @@ codepress/
|
||||
├── admin/ # Admin console
|
||||
│ ├── config/ # Admin configuration (admin.json)
|
||||
│ ├── src/AdminAuth.php # Authentication, roles, permissions
|
||||
│ ├── static/ # Static files (404.html)
|
||||
│ ├── storage/ # Logs, cache, geoip
|
||||
│ └── theme/default/ # Admin theme
|
||||
│ ├── assets/ # CSS, JS, fonts, codemirror
|
||||
@@ -80,7 +209,6 @@ codepress/
|
||||
│ │ ├── *.twig # Layout templates
|
||||
│ │ ├── partials/ # Header, navigation, footer
|
||||
│ │ └── assets/ # SCSS, CSS, JS, img
|
||||
│ └── demo/ # Demo theme
|
||||
├── plugins/ # Plugins
|
||||
│ ├── HTMLBlock/ # Example sidebar plugin
|
||||
│ └── Navigation/ # Essential navigation plugin (protected)
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
Een lichtgewicht, file-based content management systeem gebouwd met PHP (≥8.0).
|
||||
|
||||
**Versie:** 2.5.0 | **Licentie:** AGPL v3 / Commercial
|
||||
**Versie:** 2.6.1 | **Licentie:** AGPL v3 / Commercial
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 📝 **Multi-format Content** - Markdown, PHP en HTML bestanden
|
||||
- 🧭 **Dynamic Navigation** - Automatische menu generatie
|
||||
- 🌍 **Multi-language** - NL/EN/DE/FR ondersteuning
|
||||
- 🌍 **Multi-language** - NL/EN/DE ondersteuning
|
||||
- 🔍 **Search** - Volledige tekst zoekfunctie
|
||||
- 📱 **Responsive** - Bootstrap 5 thema's
|
||||
- 🔒 **Security** - 100/100 pentest score
|
||||
@@ -27,13 +27,141 @@ Een lichtgewicht, file-based content management systeem gebouwd met PHP (≥8.0)
|
||||
# Installeer dependencies
|
||||
composer install
|
||||
|
||||
# Start server met router voor schone URLs
|
||||
# Start server met router voor schone URLs (alleen lokaal)
|
||||
php -S localhost:8080 cms/router.php
|
||||
```
|
||||
|
||||
**Website:** `http://localhost:8080`
|
||||
**Admin:** `http://localhost:8080/admin` (login: `admin` / `admin`)
|
||||
|
||||
## 📦 Installatie
|
||||
|
||||
### Vereisten
|
||||
|
||||
- **PHP** ≥ 8.0 met extensies: `json`, `mbstring`
|
||||
- **Composer** (PHP dependency manager)
|
||||
- Webserver: **Apache 2.4+** met `mod_rewrite` of **Nginx** met PHP-FPM
|
||||
- Optioneel: `opcache` (aanbevolen voor performance), `git` (voor content versioning), `zip` extensie (voor ZIP backup/restore)
|
||||
|
||||
### Stap 1 — Code en dependencies
|
||||
|
||||
```bash
|
||||
git clone <repository-url> codepress
|
||||
cd codepress
|
||||
composer install
|
||||
```
|
||||
|
||||
### Stap 2 — Configuratie
|
||||
|
||||
```bash
|
||||
cp config.json.example config.json
|
||||
cp admin/config/admin.json.example admin/config/admin.json
|
||||
```
|
||||
|
||||
Pas `config.json` aan met je site titel, taal en plugins. Wijzig het admin wachtwoord in `admin/config/admin.json` (standaard `admin`/`admin`).
|
||||
|
||||
### Stap 3a — Apache 2.4+
|
||||
|
||||
De webroot is de `public/` map. Voorbeeld vhost (`/etc/apache2/sites-available/codepress.conf`):
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName example.com
|
||||
DocumentRoot /var/www/codepress/public
|
||||
|
||||
<Directory /var/www/codepress/public>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/codepress_error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/codepress_access.log combined
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
Benodigde Apache modules:
|
||||
|
||||
```bash
|
||||
sudo a2enmod rewrite headers
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
- `mod_rewrite` — voor clean URLs (`/nl/pagina`) en asset-serving
|
||||
- `mod_headers` — voor security headers
|
||||
- `AllowOverride All` — zodat `.htaccess` in `public/` wordt toegepast
|
||||
|
||||
### Stap 3b — Nginx
|
||||
|
||||
Voorbeeld server block (`/etc/nginx/sites-available/codepress`):
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
root /var/www/codepress/public;
|
||||
index index.php;
|
||||
|
||||
# Clean URLs: taal-prefixed pagina's
|
||||
location ~ ^/(nl|en|de)(/(.+))?$ {
|
||||
try_files $uri /index.php?lang=$1&page=$2;
|
||||
}
|
||||
|
||||
# Admin routes
|
||||
location /admin {
|
||||
try_files $uri /admin.php?$args;
|
||||
}
|
||||
|
||||
# Asset-serving via asset.php (themes/plugins/admin buiten webroot)
|
||||
location ~ ^/(themes|plugins)/([^/]+)/assets/(.+)$ {
|
||||
try_files $uri /asset.php;
|
||||
}
|
||||
location ~ ^/admin/assets/(.+)$ {
|
||||
try_files $uri /asset.php;
|
||||
}
|
||||
|
||||
# PHP via FPM
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass unix:/run/php/php8.0-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
# Beveiliging: blokkeer toegang tot gevoelige mappen
|
||||
location ~ ^/(content|cms|admin/src|admin/config|admin/storage|var|vendor)/ {
|
||||
deny all;
|
||||
return 403;
|
||||
}
|
||||
|
||||
location ~ /\.(git|htaccess) {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Let op:** Nginx gebruikt geen `.htaccess`. De security headers moeten in de Nginx config worden gezet:
|
||||
|
||||
```nginx
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header Referrer-Policy strict-origin-when-cross-origin;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';";
|
||||
```
|
||||
|
||||
### Stap 4 — Mappen rechten
|
||||
|
||||
Zorg dat de webserver schrijfrechten heeft op de runtime mappen:
|
||||
|
||||
```bash
|
||||
chown -R www-data:www-data var/ admin/storage/ content/
|
||||
chmod -R 755 .
|
||||
```
|
||||
|
||||
### Stap 5 — Test
|
||||
|
||||
Open de website in je browser. Bij een lege content-map zie je een welkomstpagina. De admin console is bereikbaar via `/admin` (login `admin`/`admin`).
|
||||
|
||||
## 📚 Handleidingen
|
||||
|
||||
Zie **[guide/](guide/)** voor uitgebreide documentatie per rol:
|
||||
@@ -68,6 +196,7 @@ codepress/
|
||||
├── admin/ # Admin console
|
||||
│ ├── config/ # Admin configuratie (admin.json)
|
||||
│ ├── src/AdminAuth.php # Authenticatie, rollen, permissies
|
||||
│ ├── static/ # Statische bestanden (404.html)
|
||||
│ ├── storage/ # Logs, cache, geoip
|
||||
│ └── theme/default/ # Admin thema
|
||||
│ ├── assets/ # CSS, JS, fonts, codemirror
|
||||
@@ -80,7 +209,6 @@ codepress/
|
||||
│ │ ├── *.twig # Layout templates
|
||||
│ │ ├── partials/ # Header, navigation, footer
|
||||
│ │ └── assets/ # SCSS, CSS, JS, img
|
||||
│ └── demo/ # Demo thema
|
||||
├── plugins/ # Plugins
|
||||
│ ├── HTMLBlock/ # Voorbeeld sidebar plugin
|
||||
│ └── Navigation/ # Essentiële navigatie plugin (beschermd)
|
||||
|
||||
@@ -49,6 +49,15 @@
|
||||
- [x] Pentest controles uitgevoerd (30/30 tests geslaagd — 0 vulnerabilities)
|
||||
- [x] WCAG 2.1 AA accessibility tests (25/25 tests geslaagd — 100% compliance, test-script verbeterd met min/max checks en grep -E)
|
||||
|
||||
## v2.6.1 (2026-08-17) ✅
|
||||
- [x] Pentest controles uitgevoerd (30/30)
|
||||
- [x] WCAG 2.1 AA accessibility tests (25/25)
|
||||
- [x] README.md en handleidingen in guide/ doorgeloken en bijgewerkt
|
||||
- [x] Installatie instructies toegevoegd aan README (Apache2/Nginx/PHP/composer)
|
||||
- [x] Verwijderde vendor packages: php-mqtt/client, mustache/mustache (uit composer + autoloader)
|
||||
- [x] Verslag gemaakt (docs/release-notes/v2.6.1.md)
|
||||
- [x] Versie verhoogd naar 2.6.1
|
||||
|
||||
## v2.6.0 (2026-08-15) ✅
|
||||
- [x] Pentest controles uitgevoerd (30/30)
|
||||
- [x] WCAG 2.1 AA accessibility tests (25/25)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="error-page text-center py-5">
|
||||
<div class="error-code display-1 fw-bold text-primary mb-3">404</div>
|
||||
<h1 class="h2 mb-3">Pagina niet gevonden</h1>
|
||||
<p class="text-muted mb-4">De pagina die u zoekt bestaat niet of is verplaatst.</p>
|
||||
<a href="/" class="btn btn-primary">
|
||||
<i class="bi bi-house me-1" aria-hidden="true"></i>
|
||||
Terug naar de hoofdpagina
|
||||
</a>
|
||||
</div>
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 41 KiB |
@@ -40,10 +40,14 @@
|
||||
<i class="bi bi-gear-fill"></i> {{ ta.admin_title|default('CodePress Admin') }}
|
||||
</div>
|
||||
<ul class="nav flex-column mt-2">
|
||||
{# Algemene sectie: Dashboard (plugin) + plugin items met section=general #}
|
||||
{% set general_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'general') %}
|
||||
{% if general_plugins is not empty or has_permission('dashboard') or true %}
|
||||
{# Algemene sectie: Dashboard (altijd zichtbaar) + plugin items met section=general #}
|
||||
{% set general_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'general' and item.route != 'dashboard') %}
|
||||
<li class="nav-section">{{ ta.section_general|default('Algemeen') }}</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ route == 'dashboard' or route == '' ? 'active' : '' }}" href="/admin/dashboard">
|
||||
<i class="bi bi-speedometer2"></i> {{ ta.dashboard|default('Dashboard') }}
|
||||
</a>
|
||||
</li>
|
||||
{% for item in general_plugins %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ route == item.route or route starts with item.route ~ '/' ? 'active' : '' }}" href="/admin/{{ item.route }}">
|
||||
@@ -51,7 +55,6 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if has_permission('content') %}
|
||||
<li class="nav-section">{{ ta.section_content|default('Content') }}</li>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends "layouts/admin.twig" %}
|
||||
|
||||
{% block title %}{{ route|capitalize }} - CodePress Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ plugin_content|raw }}
|
||||
{% endblock %}
|
||||
@@ -13,6 +13,21 @@
|
||||
<input type="text" class="form-control" id="name" name="name" required autofocus>
|
||||
<small class="form-text text-muted">{{ ta.plugin_name_help|default('Alleen letters, cijfers, underscores en streepjes.') }}</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Plugin type</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="type" id="type-content" value="content" checked>
|
||||
<label class="form-check-label" for="type-content">
|
||||
<strong>Content plugin</strong> — Verschijnt in de sidebar op content pagina's
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="type" id="type-system" value="system">
|
||||
<label class="form-check-label" for="type-system">
|
||||
<strong>Systeem plugin</strong> — Voegt functionaliteit toe aan het CMS (admin menu, API)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# WCAG 2.1 AA Accessibility Test Suite for CodePress CMS
|
||||
# Tests for web accessibility compliance
|
||||
|
||||
BASE_URL="http://localhost:8080"
|
||||
BASE_URL="http://development.codepress.noorlander.info"
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
FAILED_TESTS=0
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# CodePress CMS Functional Test Suite v1.5.0
|
||||
# Tests core functionality, new features, and regressions
|
||||
|
||||
BASE_URL="http://localhost:8080"
|
||||
BASE_URL="http://development.codepress.noorlander.info"
|
||||
TEST_DATE=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
TOTAL_TESTS=0
|
||||
PASSED_TESTS=0
|
||||
@@ -80,14 +80,11 @@ echo ""
|
||||
echo "2. CONTENT RENDERING TESTS"
|
||||
echo "--------------------------"
|
||||
|
||||
# Test Markdown content
|
||||
run_test "Markdown rendering" "curl -s '$BASE_URL/?page=demo/content-only' | grep -c '<h1>'" "1"
|
||||
# Test Markdown content (guide page uses Markdown)
|
||||
run_test "Markdown rendering" "curl -s '$BASE_URL/?guide' | grep -c '<h1>'" "1"
|
||||
|
||||
# Test HTML content
|
||||
run_test "HTML content" "curl -s '$BASE_URL/?page=demo/html-demo' | grep -c '<h1>'" "1"
|
||||
|
||||
# Test PHP content
|
||||
run_test "PHP content" "curl -s '$BASE_URL/?page=demo/php-demo' | grep -c 'PHP Version'" "1"
|
||||
# Test 404 handling for non-existent pages
|
||||
run_test "404 content handling" "curl -s '$BASE_URL/?page=nonexistent' | grep -c '404'" "1"
|
||||
|
||||
echo ""
|
||||
echo "3. NAVIGATION TESTS"
|
||||
@@ -97,7 +94,7 @@ echo "-------------------"
|
||||
run_test "Menu generation" "curl -s '$BASE_URL/' | grep -c 'nav-item'" "2"
|
||||
|
||||
# Test breadcrumb navigation
|
||||
run_test "Breadcrumb navigation" "curl -s '$BASE_URL/?page=demo/content-only' | grep -c 'breadcrumb'" "1"
|
||||
run_test "Breadcrumb navigation" "curl -s '$BASE_URL/?guide' | grep -c 'breadcrumb'" "1"
|
||||
|
||||
echo ""
|
||||
echo "4. TEMPLATE SYSTEM TESTS"
|
||||
@@ -243,7 +240,6 @@ Functional testing performed on CodePress CMS v1.5.0 covering core functionality
|
||||
|
||||
### Plugin System
|
||||
- **HTMLBlock Plugin**: Custom HTML blocks in sidebar
|
||||
- **MQTTTracker Plugin**: Real-time analytics and tracking
|
||||
- **Plugin Manager**: Centralized plugin loading system
|
||||
|
||||
### Enhanced Documentation
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# CodePress CMS Penetration Test Script
|
||||
# WARNING: Only run this on systems you have permission to test!
|
||||
|
||||
TARGET="http://localhost:8080"
|
||||
TARGET="http://development.codepress.noorlander.info"
|
||||
RESULTS_FILE="pentest_results.txt"
|
||||
|
||||
echo "🔒 CodePress CMS Penetration Test" > $RESULTS_FILE
|
||||
|
||||
@@ -449,10 +449,29 @@ class CodePressCMS {
|
||||
if (isset($_GET['guide']) || $pageCheck === 'guide') {
|
||||
return $this->getGuidePage();
|
||||
}
|
||||
|
||||
// Determine if the request has a valid language prefix
|
||||
$availableLangs = array_keys($this->getAvailableLanguages());
|
||||
$requestPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/';
|
||||
$requestPath = ltrim($requestPath, '/');
|
||||
$langFromUrl = '';
|
||||
if ($requestPath !== '' && in_array(explode('/', $requestPath)[0], $availableLangs, true)) {
|
||||
$langFromUrl = explode('/', $requestPath)[0];
|
||||
}
|
||||
|
||||
// No language prefix and not the root → 404
|
||||
if ($langFromUrl === '' && $requestPath !== '' && $requestPath !== 'favicon.ico') {
|
||||
return $this->getError404();
|
||||
}
|
||||
|
||||
// Check if content directory is empty
|
||||
// Check if content directory is empty — show welcome page for new installations
|
||||
if ($this->isContentDirEmpty()) {
|
||||
return $this->getGuidePage();
|
||||
$requestedPage = $_GET['page'] ?? '';
|
||||
// Only show welcome page on the home URL; everything else is a 404
|
||||
if (empty($requestedPage) || $requestedPage === $this->getEffectiveDefaultPage()) {
|
||||
return $this->getWelcomePage();
|
||||
}
|
||||
return $this->getError404();
|
||||
}
|
||||
|
||||
$page = $_GET['page'] ?? $this->getEffectiveDefaultPage();
|
||||
@@ -1048,9 +1067,60 @@ class CodePressCMS {
|
||||
$files = scandir($contentDir);
|
||||
$files = array_diff($files, ['.', '..']);
|
||||
|
||||
// Filter out hidden files (e.g. .gitkeep) — only count visible content
|
||||
$files = array_filter($files, function($f) {
|
||||
return $f[0] !== '.';
|
||||
});
|
||||
|
||||
return empty($files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get welcome page content for new installations (empty content directory)
|
||||
*
|
||||
* Shows a clear "new installation" message with next steps when the
|
||||
* content directory has no content files yet.
|
||||
*
|
||||
* @return array Welcome page data
|
||||
*/
|
||||
private function getWelcomePage() {
|
||||
$adminUrl = $this->buildAdminUrl();
|
||||
$guideUrl = '/' . $this->currentLanguage . '/guide';
|
||||
|
||||
$content = '<div class="welcome-page">';
|
||||
$content .= '<h1>' . htmlspecialchars($this->t('welcome_title')) . '</h1>';
|
||||
$content .= '<p class="alert alert-info" role="alert">';
|
||||
$content .= '<i class="bi bi-info-circle me-2" aria-hidden="true"></i>';
|
||||
$content .= htmlspecialchars($this->t('welcome_intro'));
|
||||
$content .= '</p>';
|
||||
|
||||
$content .= '<h2>' . htmlspecialchars($this->t('welcome_next_steps')) . '</h2>';
|
||||
$content .= '<ol class="list-group list-group-numbered mb-4">';
|
||||
$content .= '<li class="list-group-item">' . htmlspecialchars($this->t('welcome_step_1')) . '</li>';
|
||||
$content .= '<li class="list-group-item">' . htmlspecialchars($this->t('welcome_step_2')) . '</li>';
|
||||
$content .= '<li class="list-group-item">' . htmlspecialchars($this->t('welcome_step_3')) . '</li>';
|
||||
$content .= '</ol>';
|
||||
|
||||
$content .= '<div class="d-flex flex-column flex-md-row gap-2 mb-4">';
|
||||
$content .= '<a href="' . htmlspecialchars($adminUrl) . '" class="btn btn-primary">';
|
||||
$content .= '<i class="bi bi-gear me-1" aria-hidden="true"></i> ';
|
||||
$content .= htmlspecialchars($this->t('welcome_admin_link'));
|
||||
$content .= '</a>';
|
||||
$content .= '<a href="' . htmlspecialchars($guideUrl) . '" class="btn btn-outline-secondary">';
|
||||
$content .= '<i class="bi bi-book me-1" aria-hidden="true"></i> ';
|
||||
$content .= htmlspecialchars($this->t('welcome_guide_link'));
|
||||
$content .= '</a>';
|
||||
$content .= '</div>';
|
||||
$content .= '</div>';
|
||||
|
||||
return [
|
||||
'title' => $this->t('welcome_title'),
|
||||
'content' => $content,
|
||||
'layout' => 'full_content',
|
||||
'metadata' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get guide page content based on user language
|
||||
*
|
||||
@@ -1203,9 +1273,22 @@ class CodePressCMS {
|
||||
$content .= '<p>' . $this->t('directory_empty') . '.</p>';
|
||||
}
|
||||
|
||||
// Check for index.md in this directory for layout/plugins metadata
|
||||
$indexFile = $dirPath . '/index.md';
|
||||
$layout = 'full_content';
|
||||
$metadata = [];
|
||||
if (file_exists($indexFile)) {
|
||||
$indexContent = file_get_contents($indexFile);
|
||||
$parsed = $this->parseMetadata($indexContent);
|
||||
$metadata = $parsed['metadata'];
|
||||
$layout = $metadata['layout'] ?? 'full_content';
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'content' => $content
|
||||
'content' => $content,
|
||||
'layout' => $layout,
|
||||
'metadata' => $metadata,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1215,9 +1298,17 @@ class CodePressCMS {
|
||||
* @return array 404 page data
|
||||
*/
|
||||
private function getError404() {
|
||||
$staticFile = __DIR__ . '/../../../admin/static/404.html';
|
||||
$body = file_exists($staticFile)
|
||||
? file_get_contents($staticFile)
|
||||
: '<h1>404 - ' . $this->t('page_not_found') . '</h1><p>' . $this->t('page_not_found_text') . '</p>';
|
||||
|
||||
return [
|
||||
'title' => $this->t('page_not_found'),
|
||||
'content' => '<h1>404 - ' . $this->t('page_not_found') . '</h1><p>' . $this->t('page_not_found_text') . '</p>'
|
||||
'content' => $body,
|
||||
'layout' => 'full_content',
|
||||
'metadata' => [],
|
||||
'is_404' => true,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1239,6 +1330,11 @@ class CodePressCMS {
|
||||
$page = $this->getPage();
|
||||
$this->pluginManager->doAction('onPageLoad', $page);
|
||||
$this->pluginManager->doAction('onBeforeRender');
|
||||
|
||||
// Set 404 status for not-found pages (rendered through the theme)
|
||||
if (!empty($page['is_404'])) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
$menu = $this->getMenu();
|
||||
|
||||
@@ -1489,7 +1585,7 @@ class CodePressCMS {
|
||||
$isExpanded = $this->folderContainsActivePage($item['children']);
|
||||
|
||||
if ($level === 0) {
|
||||
// Root level folders
|
||||
// Root level folders - Bootstrap dropdown
|
||||
$html .= '<li class="nav-item dropdown">';
|
||||
$html .= '<a class="nav-link dropdown-toggle" href="#" id="' . $folderId . '" role="button" data-bs-toggle="dropdown" aria-expanded="' . ($isExpanded ? 'true' : 'false') . '">';
|
||||
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>';
|
||||
@@ -1499,12 +1595,12 @@ class CodePressCMS {
|
||||
$html .= '</ul>';
|
||||
$html .= '</li>';
|
||||
} else {
|
||||
// Nested folders in dropdown
|
||||
$html .= '<li class="dropdown-submenu">';
|
||||
$html .= '<a class="dropdown-item dropdown-toggle" href="#" id="' . $folderId . '">';
|
||||
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>';
|
||||
// Nested folders - CSS hover submenu (unlimited depth)
|
||||
$html .= '<li class="dropdown-submenu' . ($isExpanded ? ' show' : '') . '">';
|
||||
$html .= '<a class="dropdown-item dropdown-toggle" href="#" id="' . $folderId . '" role="button" aria-expanded="' . ($isExpanded ? 'true' : 'false') . '">';
|
||||
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-right"></i>';
|
||||
$html .= '</a>';
|
||||
$html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">';
|
||||
$html .= '<ul class="dropdown-menu' . ($isExpanded ? ' show' : '') . '" aria-labelledby="' . $folderId . '">';
|
||||
$html .= $this->renderMenu($item['children'], $level + 1);
|
||||
$html .= '</ul>';
|
||||
$html .= '</li>';
|
||||
|
||||
@@ -169,8 +169,22 @@ class ThemeManager {
|
||||
$compiler = new Compiler();
|
||||
$compiler->setImportPaths($this->themeDir . '/assets/scss');
|
||||
$css = $compiler->compileString(file_get_contents($scssFile))->getCss();
|
||||
|
||||
// Remove read-only before writing (file may be locked from previous compile)
|
||||
if (is_file($outFile)) {
|
||||
@chmod($outFile, 0644);
|
||||
}
|
||||
if (is_file($cacheFile)) {
|
||||
@chmod($cacheFile, 0644);
|
||||
}
|
||||
|
||||
file_put_contents($outFile, $css);
|
||||
file_put_contents($cacheFile, (string)$mtime);
|
||||
|
||||
// Set read-only to prevent manual edits
|
||||
@chmod($outFile, 0444);
|
||||
@chmod($cacheFile, 0444);
|
||||
|
||||
return $outFile;
|
||||
} catch (\Throwable $e) {
|
||||
error_log('ThemeManager SCSS compile error: ' . $e->getMessage());
|
||||
@@ -180,20 +194,15 @@ class ThemeManager {
|
||||
|
||||
/**
|
||||
* Get the public URL for the theme CSS.
|
||||
* Priority: 1) assets/css/theme.css (manual), 2) assets/css_compiled/theme.css (compiled), 3) null
|
||||
* Uses assets/css_compiled/theme.css (compiled from SCSS by scssphp).
|
||||
*/
|
||||
public function getCssUrl(): ?string {
|
||||
$manualCss = $this->themeDir . '/assets/css/theme.css';
|
||||
$compiledCss = $this->themeDir . '/assets/css_compiled/theme.css';
|
||||
|
||||
if (is_file($manualCss)) {
|
||||
return $this->getThemeAssetUrl('css/theme.css');
|
||||
}
|
||||
|
||||
|
||||
if (is_file($compiledCss)) {
|
||||
return $this->getThemeAssetUrl('css_compiled/theme.css');
|
||||
}
|
||||
|
||||
|
||||
$compiled = $this->compileCss();
|
||||
if ($compiled !== null) {
|
||||
return $this->getThemeAssetUrl('css_compiled/theme.css');
|
||||
|
||||
@@ -697,59 +697,6 @@
|
||||
},
|
||||
"time": "2026-01-13T17:56:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mustache/mustache",
|
||||
"version": "v3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bobthecow/mustache.php.git",
|
||||
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/176b6b21d68516dd5107a63ab71b0050e518b7a4",
|
||||
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "~2.19.3",
|
||||
"yoast/phpunit-polyfills": "^2.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Mustache\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"src/compat.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Justin Hileman",
|
||||
"email": "justin@justinhileman.info",
|
||||
"homepage": "http://justinhileman.com"
|
||||
}
|
||||
],
|
||||
"description": "A Mustache implementation in PHP.",
|
||||
"homepage": "https://github.com/bobthecow/mustache.php",
|
||||
"keywords": [
|
||||
"mustache",
|
||||
"templating"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/bobthecow/mustache.php/issues",
|
||||
"source": "https://github.com/bobthecow/mustache.php/tree/v3.0.0"
|
||||
},
|
||||
"time": "2025-06-28T18:28:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "myclabs/php-enum",
|
||||
"version": "1.8.5",
|
||||
@@ -967,63 +914,6 @@
|
||||
},
|
||||
"time": "2025-10-31T00:45:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-mqtt/client",
|
||||
"version": "v2.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-mqtt/client.git",
|
||||
"reference": "3d141846753a0adee265680ae073cfb9030f2390"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-mqtt/client/zipball/3d141846753a0adee265680ae073cfb9030f2390",
|
||||
"reference": "3d141846753a0adee265680ae073cfb9030f2390",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"myclabs/php-enum": "^1.7",
|
||||
"php": "^8.0",
|
||||
"psr/log": "^1.1|^2.0|^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/php-invoker": "^3.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"squizlabs/php_codesniffer": "^3.5"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-redis": "Required for the RedisRepository"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpMqtt\\Client\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Marvin Mall",
|
||||
"email": "marvin-mall@msn.com",
|
||||
"role": "developer"
|
||||
}
|
||||
],
|
||||
"description": "An MQTT client written in and for PHP.",
|
||||
"keywords": [
|
||||
"client",
|
||||
"mqtt",
|
||||
"publish",
|
||||
"subscribe"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-mqtt/client/issues",
|
||||
"source": "https://github.com/php-mqtt/client/tree/v2.3.0"
|
||||
},
|
||||
"time": "2025-09-30T17:53:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/event-dispatcher",
|
||||
"version": "1.0.0",
|
||||
@@ -1837,10 +1727,10 @@
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# CodePress CMS v2.6.1 — Release Notes
|
||||
|
||||
**Release datum:** 2026-08-17
|
||||
**Codename:** Lyra
|
||||
**Status:** stable
|
||||
|
||||
---
|
||||
|
||||
## Nieuwe features
|
||||
|
||||
### Welkomstpagina bij lege content-map (nieuwe installatie)
|
||||
- Wanneer de `content/` map leeg is (nieuwe installatie), toont de CMS nu een duidelijke welkomstpagina
|
||||
- De pagina bevat een introductietekst, een lijst met volgende stappen en knoppen naar de admin console en handleiding
|
||||
- Vertaald in NL, EN en DE (`welcome_*` keys in `language/*/site.php`)
|
||||
- `isContentDirEmpty()` filtert nu verborgen bestanden (zoals `.gitkeep`) zodat een map met alleen een `.gitkeep` als leeg wordt herkend
|
||||
|
||||
### 404-afhandeling binnen het actieve theme
|
||||
- Nieuwe statische 404 pagina in `admin/static/404.html` met een link naar de home pagina
|
||||
- De 404 inhoud wordt binnen het actieve theme gerenderd (met header, navigatie, footer en Bootstrap styling)
|
||||
- `getError404()` laadt de inhoud uit `admin/static/404.html` en geeft deze als page content terug (layout `full_content`)
|
||||
- `render()` zet de juiste `http_response_code(404)` status bij niet-gevonden pagina's
|
||||
|
||||
### HTTP 404 status bij onbekende pagina's en missende taalprefix
|
||||
- `getPage()` geeft een 404 als de URL geen geldige taalprefix bevat (bijv. `/random-page` in plaats van `/nl/random-page`)
|
||||
- Niet-bestaande pagina's binnen een taal (`/nl/nonexistent`) geven nu ook een 404 in plaats van HTTP 200
|
||||
- Eerder werd bij een lege content-map altijd de welkomstpagina getoond, ook voor niet-bestaande pagina's — dit is nu beperkt tot de home URL
|
||||
|
||||
---
|
||||
|
||||
## Verbeteringen
|
||||
|
||||
### Test scripts
|
||||
- Test scripts (`pentest.sh`, `accessibility.sh`, `run-tests.sh`) gebruiken nu de Apache-URL (`http://development.codepress.noorlander.info`) in plaats van `localhost:8080`
|
||||
- Functionele tests opgeschoond: tests die naar niet-bestaande `demo/` content pagina's verwezen vervangen door werkende tests
|
||||
|
||||
### Documentatie
|
||||
- `README.md` en `README.en.md` bijgewerkt: versie naar 2.6.1, multi-language talen gecorrigeerd (NL/EN/DE), `admin/static/` map toegevoegd aan project structuur
|
||||
- Guide index bestanden (`guide/nl/index.md`, `guide/en/index.md`) bijgewerkt naar versie 2.6.1
|
||||
- `guide/*/codepress-developer.md` versie referenties bijgewerkt naar 2.6.1
|
||||
- `AGENTS.md` samengevoegd naar de root map (`./AGENTS.md`); `./development/AGENTS.md` is verwijderd
|
||||
- AGENTS.md verduidelijkt welke URL bij welke map hoort en dat tests via Apache draaien (niet via PHP dev server)
|
||||
|
||||
---
|
||||
|
||||
## Opschoning
|
||||
|
||||
### Verwijderde ongebruikte bestanden
|
||||
- `package.json` — was voor NPM build (`npm run build:css`), runtime gebruikt scssphp (PHP) voor SCSS compilatie
|
||||
- `src/scss/` map — was voor NPM sass build, overbodig na verwijderen van `package.json`
|
||||
- `.htaccess` (root) — Apache docroot is `public/`, deze root `.htaccess` werd niet geserveerd
|
||||
- `themes/demo/` — niet actief in `config.json` (gebruikt `default`), alleen genoemd als voorbeeld
|
||||
|
||||
### Opschoning referenties
|
||||
- `README.md` en `README.en.md` — demo theme referenties verwijderd uit project structuur
|
||||
- `guide/*/codepress-developer/architectuur.md` — demo verwijdering uit mappenstructuur boom
|
||||
- `themes/gen-preview.sh` — hardcoded `default demo test` loop vervangen door dynamische `*/` loop
|
||||
- `.gitignore` — `node_modules/`, `package-lock.json` en `.sass-cache/` regels verwijderd (NPM niet meer gebruikt)
|
||||
|
||||
### Verwijderde vendor packages
|
||||
- `mustache/mustache` — niet meer gebruikt (Twig is de template engine)
|
||||
- `php-mqtt/client` — niet meer gebruikt
|
||||
- Beide packages verwijderd uit `composer.lock`, `vendor/composer/installed.json`, `installed.php`, en de autoloader bestanden
|
||||
- `MQTTTracker` referentie verwijderd uit functionele test script
|
||||
|
||||
### Installatie documentatie
|
||||
- `README.md` en `README.en.md` bevatten nu een volledige installatie sectie met:
|
||||
- Vereisten (PHP ≥8.0, composer, extensies)
|
||||
- Apache 2.4+ vhost voorbeeld met `mod_rewrite`, `mod_headers`, `AllowOverride All`
|
||||
- Nginx server block voorbeeld met PHP-FPM, clean URLs, asset-serving en security headers
|
||||
- Mappen rechten en test instructies
|
||||
|
||||
---
|
||||
|
||||
## Beveiliging
|
||||
- Pentest suite: 30/30 tests geslaagd (XSS, path traversal, PHP injection, null byte, command injection, template injection, HTTP header injection, information disclosure, security headers, DoS)
|
||||
- Security headers aanwezig: X-Frame-Options, Content-Security-Policy, X-Content-Type-Options
|
||||
- Geen vulnerabilities gedetecteerd
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
- WCAG 2.1 AA: 25/25 tests geslaagd (100%)
|
||||
- ARIA landmarks aanwezig (24 per pagina)
|
||||
- Semantic HTML structuur (header, nav, main, footer)
|
||||
- Skip-to-content links, focus indicators, screen reader support
|
||||
- Mobile viewport en touch targets aanwezig
|
||||
|
||||
---
|
||||
|
||||
## Systeem vereisten
|
||||
- PHP >= 8.0
|
||||
- Extensies: json, mbstring
|
||||
- Optioneel: opcache (aanbevolen voor performance)
|
||||
- Git (optioneel, voor content versioning feature)
|
||||
- ZipArchive (optioneel, voor ZIP backup/restore feature)
|
||||
- Apache met `mod_rewrite` en `AllowOverride All` (voor clean URLs en asset-serving)
|
||||
|
||||
---
|
||||
|
||||
## Upgraden
|
||||
1. Maak een backup van de huidige content map
|
||||
2. Pull de nieuwe code
|
||||
3. Run `composer install` om dependencies bij te werken
|
||||
4. Test de website
|
||||
5. Optioneel: initialiseer git in content/ via Admin → Backup & Restore → Git init
|
||||
|
||||
---
|
||||
|
||||
## Test resultaten samenvatting
|
||||
|
||||
| Test | Resultaat |
|
||||
|------|----------|
|
||||
| Pentest | 30/30 geslaagd, 0 vulnerabilities |
|
||||
| WCAG 2.1 AA Accessibility | 25/25 geslaagd, 100% compliance |
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
The admin manager guide contains the following topics:
|
||||
|
||||
- **Dashboard** - Website overview
|
||||
- **Content management** - Managing files and pages
|
||||
- **Configuration** - Site settings
|
||||
- **Theme management** - Managing and creating themes
|
||||
- **Security** - Bot protection and sessions
|
||||
- **Plugins** - Managing and creating plugins
|
||||
- **Users** - Adding and removing users
|
||||
- **Statistics** - Viewing visitor statistics
|
||||
- **Dashboard** - Role-based overview of the website (Admin, Content Manager, BI Manager, Site Admin)
|
||||
- **Content management** - Managing files and pages with layout selection and plugins
|
||||
- **Configuration** - Site settings (title, language, author, analytics, logging)
|
||||
- **Theme management** - Managing, activating and creating themes (SCSS compilation)
|
||||
- **Security** - Bot protection, rate limiting and session settings
|
||||
- **Plugins** - Managing plugins: content (sidebar) and system (admin menu/API)
|
||||
- **Users** - User management with roles (Admin, Content Manager, BI Manager, Site Admin)
|
||||
- **Statistics** - Viewing and exporting visitor statistics
|
||||
- **Logs** - Viewing and filtering log files
|
||||
- **Media** - Managing media files
|
||||
- **Update** - Check for updates
|
||||
|
||||
@@ -2,16 +2,45 @@
|
||||
|
||||
## Managing files
|
||||
|
||||
- **Upload** - Upload media files
|
||||
- **New folder** - Create folder structure
|
||||
- **New file** - Create a page
|
||||
- **Edit** - Modify existing content
|
||||
- **Rename** - Change file names
|
||||
- **Move** - Move content
|
||||
- **Delete** - Remove content
|
||||
- **New folder** — Create folder structure
|
||||
- **New file** — Create a page (`.md`, `.php`, `.html`)
|
||||
- **Edit** — Modify existing content in the CodeMirror editor
|
||||
- **Rename** — Change file or folder names
|
||||
- **Move** — Move content to another folder
|
||||
- **Delete** — Remove content
|
||||
- **Rename folder** — Change a folder name
|
||||
|
||||
## Editor
|
||||
## Editor (content-edit)
|
||||
|
||||
- CodeMirror with syntax highlighting
|
||||
- Toolbar for Markdown formatting
|
||||
- Shortcuts: Ctrl+S (save), Ctrl+N (new)
|
||||
- **CodeMirror** with syntax highlighting (Markdown, PHP, HTML)
|
||||
- **Toolbar** for quickly inserting Markdown
|
||||
- **Shortcuts**: Ctrl+S (save), Ctrl+N (new)
|
||||
|
||||
## Layout selection
|
||||
|
||||
On the content-edit page you can choose the layout from the layouts defined in `theme.json` (template mapping). The selected layout is stored in the frontmatter `layout:` key.
|
||||
|
||||
## Plugins on pages
|
||||
|
||||
- Content plugins (from `plugin.json` with `type: "content"`) appear in the plugin selection
|
||||
- Choose which plugins appear in the sidebar
|
||||
- **Order is adjustable** with up/down buttons
|
||||
- The plugin order is stored in the frontmatter `plugins:` key
|
||||
- Plugins are **hidden** when the chosen layout has no sidebar (e.g. `full_content`)
|
||||
|
||||
## Frontmatter
|
||||
|
||||
The editor updates the frontmatter live when layout or plugin selection changes:
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
plugins:
|
||||
- HTMLBlock
|
||||
- Navigation
|
||||
---
|
||||
|
||||
# Page title
|
||||
|
||||
Content...
|
||||
```
|
||||
@@ -1,10 +1,32 @@
|
||||
# Dashboard
|
||||
|
||||
The dashboard shows an overview of:
|
||||
The dashboard shows a **role-based overview** of the website. Which widgets are visible depends on the role of the logged-in user.
|
||||
|
||||
- Views (30 days)
|
||||
- Unique visitors
|
||||
## Roles and widgets
|
||||
|
||||
### Admin
|
||||
- Views (30 days) and unique visitors
|
||||
- Number of pages and folders
|
||||
- Active plugins
|
||||
- Recent activity
|
||||
- Quick actions
|
||||
- System information
|
||||
- Quick actions (new page, plugin, theme)
|
||||
|
||||
### Content Manager
|
||||
- Number of pages and folders
|
||||
- Recent content changes
|
||||
- Quick actions (new page, folder)
|
||||
|
||||
### BI Manager
|
||||
- Views (30 days) and unique visitors
|
||||
- Top pages
|
||||
- Recent visits
|
||||
|
||||
### Site Admin
|
||||
- Active plugins and themes
|
||||
- System information
|
||||
- Quick actions (theme, plugin, update)
|
||||
|
||||
## Access
|
||||
|
||||
The dashboard is the default page after login (`/admin/dashboard`). All roles have access to the dashboard.
|
||||
@@ -1,13 +1,40 @@
|
||||
# Users
|
||||
|
||||
## Add user
|
||||
Users are stored in `admin/config/admin.json` (file-based, no database). Each user has a **role** that determines which admin routes and sidebar items are visible.
|
||||
|
||||
## Roles
|
||||
|
||||
CodePress has four roles, defined in `AdminAuth::ROLE_PERMISSIONS`:
|
||||
|
||||
| Role | Label | Permissions |
|
||||
|------|-------|-------------|
|
||||
| `admin` | Admin | Everything (`*`) |
|
||||
| `content-manager` | Content Manager | Content management, guide |
|
||||
| `bi-manager` | BI Manager | Statistics, logs, guide |
|
||||
| `site-admin` | Site Admin | Theme, plugins, statistics, logs, update, guide |
|
||||
|
||||
Roles are displayed with their label via `AdminAuth::ROLE_LABELS`.
|
||||
|
||||
## Adding a user
|
||||
|
||||
1. Go to **Users**
|
||||
2. Enter username
|
||||
3. Choose password
|
||||
4. Click **Add**
|
||||
3. Choose password (stored as bcrypt hash)
|
||||
4. Select a role
|
||||
5. Click **Add**
|
||||
|
||||
## Delete user
|
||||
## Editing a user
|
||||
|
||||
- Cannot delete own account
|
||||
- Confirm with password
|
||||
- Change password (new bcrypt hash)
|
||||
- Change role (immediately affects visible routes and sidebar items)
|
||||
|
||||
## Deleting a user
|
||||
|
||||
- Not possible for own account
|
||||
- Confirm with password
|
||||
|
||||
## Access control
|
||||
|
||||
- Route access is checked in `public/admin.php` via `AdminAuth::hasPermission()`
|
||||
- Unauthorized routes return a **403 error**
|
||||
- Sidebar items are conditionally shown via the `has_permission()` Twig function in `admin.twig`
|
||||
@@ -2,20 +2,65 @@
|
||||
|
||||
## Managing themes
|
||||
|
||||
1. Go to **Theme** in admin menu
|
||||
2. **Activate** - Choose active theme
|
||||
3. **Compile SCSS** - Process SCSS to CSS
|
||||
4. **New theme** - Create custom theme
|
||||
1. Go to **Theme** in the admin menu
|
||||
2. **Activate** — Choose the active theme (stored in `config.json`)
|
||||
3. **Compile SCSS** — Process SCSS to CSS (forced)
|
||||
4. **New theme** — Create a custom theme via admin or manually
|
||||
|
||||
## Theme structure
|
||||
|
||||
```
|
||||
themes/default/
|
||||
├── theme.json # Theme configuration
|
||||
├── base.twig # Main layout
|
||||
├── full_content.twig # Layouts
|
||||
├── left_sidebar.twig
|
||||
├── right_sidebar.twig
|
||||
├── partials/ # Header, nav, footer
|
||||
└── assets/ # CSS, JS, images
|
||||
```
|
||||
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||
├── base.twig # Main layout (head, header, nav, breadcrumb, footer)
|
||||
├── full_content.twig # Layout: full width
|
||||
├── left_sidebar.twig # Layout: sidebar on the left
|
||||
├── right_sidebar.twig # Layout: sidebar on the right
|
||||
├── custom1.twig # Layout: custom
|
||||
├── guide.twig # Layout: guide with sidebar (Navigation plugin)
|
||||
├── partials/ # header.twig, navigation.twig, footer.twig
|
||||
└── assets/
|
||||
├── scss/theme.scss # SCSS source (only CSS source — manual css/theme.css must not exist)
|
||||
├── css_compiled/ # Generated by scssphp (read-only, do not edit manually)
|
||||
├── css/ # External CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
|
||||
├── js/ # app.js, bootstrap.bundle.min.js
|
||||
├── fonts/ # bootstrap-icons.woff, woff2
|
||||
└── img/ # favicon, icon, world-map
|
||||
```
|
||||
|
||||
## theme.json
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "default",
|
||||
"config": {
|
||||
"default_template": "full_content"
|
||||
},
|
||||
"template": {
|
||||
"full_content": "full_content.twig",
|
||||
"left_sidebar": "left_sidebar.twig",
|
||||
"right_sidebar": "right_sidebar.twig",
|
||||
"custom1": "custom1.twig",
|
||||
"guide": "guide.twig"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `title` — Display name in admin
|
||||
- `config.default_template` — Default layout for pages without a `layout:` frontmatter
|
||||
- `template` — Mapping from layout name to `.twig` file
|
||||
|
||||
## SCSS compilation
|
||||
|
||||
- `ThemeManager` compiles `assets/scss/theme.scss` at runtime to `assets/css_compiled/theme.css` via scssphp
|
||||
- `css_compiled/` is **read-only** — do not edit manually
|
||||
- `assets/css/theme.css` must **not** exist; otherwise `ThemeManager::getCssUrl()` ignores the SCSS
|
||||
- After SCSS changes: remove `assets/css_compiled/theme.css` and `.mtime` to force recompilation
|
||||
|
||||
## Layouts
|
||||
|
||||
Layouts are chosen via the frontmatter `layout:` key in content files. Unknown layouts fall back to `config.default_template` from `theme.json`.
|
||||
|
||||
## guide.twig
|
||||
|
||||
Special layout for guides. Injects the Navigation plugin into the sidebar for sidebar navigation. Automatically used for pages in the `guide/` folder.
|
||||
@@ -2,13 +2,20 @@
|
||||
|
||||
The CodePress developer guide contains the following topics:
|
||||
|
||||
- **Architecture** - CMS architecture and folder structure
|
||||
- **Core classes** - CodePressCMS, ThemeManager, PluginManager
|
||||
- **Plugin development** - Developing plugins
|
||||
- **Routing** - Frontend and admin routing
|
||||
- **Security** - XSS, CSRF, path traversal
|
||||
- **Testing** - Penetration, accessibility and functional tests
|
||||
- **Debugging** - Logging and cache
|
||||
- **Performance** - OPcache and SCSS caching
|
||||
- **Architecture** — CMS architecture and folder structure
|
||||
- **Core classes** — CodePressCMS, ThemeManager, PluginManager, AdminAuth
|
||||
- **Plugin development** — Plugin types (content/system), structure, API
|
||||
- **Routing** — Frontend, admin and plugin admin routing
|
||||
- **Security** — XSS, CSRF, path traversal, RBAC
|
||||
- **Testing** — Penetration, accessibility and functional tests
|
||||
- **Debugging** — Logging and cache
|
||||
- **Performance** — OPcache and SCSS caching
|
||||
|
||||
Important concepts in CodePress 2.6.1:
|
||||
|
||||
- **Plugin types** — Content plugins (sidebar) and system plugins (admin menu, routes, API)
|
||||
- **Admin plugin API** — System plugins can register admin menu items and routes
|
||||
- **SCSS compilation** — `ThemeManager` compiles SCSS to `css_compiled/` via scssphp (read-only)
|
||||
- **Asset serving** — `public/asset.php` serves themes/, admin/assets/ and plugins/ on Apache (instead of PHP dev router)
|
||||
|
||||
Select a topic from the navigation on the left.
|
||||
@@ -11,7 +11,7 @@ codepress/
|
||||
│ ├── config/ # app.php, admin.json
|
||||
│ ├── src/ # AdminAuth
|
||||
│ └── theme/default/views/ # Twig templates
|
||||
├── themes/ # Themes (default, demo, ...)
|
||||
├── themes/ # Themes (default, ...)
|
||||
├── plugins/ # Plugins (Dashboard, HTMLBlock, Navigation, ...)
|
||||
├── content/ # Content files (.md, .php, .html)
|
||||
├── language/ # Language files (nl/, en/, de/)
|
||||
|
||||
@@ -2,18 +2,60 @@
|
||||
|
||||
## Frontend routing
|
||||
|
||||
Via `cms/router.php` for PHP dev server:
|
||||
Frontend routing goes through `cms/router.php` (PHP dev server) or `.htaccess` (Apache). Both provide clean URLs.
|
||||
|
||||
```php
|
||||
// Clean URLs: /nl/page
|
||||
// Query: ?page=page&lang=nl
|
||||
### PHP dev server
|
||||
|
||||
Start the server with:
|
||||
|
||||
```bash
|
||||
php -S localhost:8080 cms/router.php
|
||||
```
|
||||
|
||||
`cms/router.php` serves:
|
||||
- Clean URLs: `/nl/page` → `public/index.php?page=page&lang=nl`
|
||||
- `themes/` assets
|
||||
- `admin/assets/` assets
|
||||
- `plugins/` assets
|
||||
|
||||
### Apache (live server)
|
||||
|
||||
`public/.htaccess` rewrites URLs to `public/index.php`. Asset URLs (`/themes/`, `/admin/assets/`, `/plugins/`) are forwarded to `public/asset.php`.
|
||||
|
||||
`public/asset.php` serves files from the correct folders with the correct MIME type:
|
||||
- `/themes/<name>/assets/...` → `themes/<name>/assets/...`
|
||||
- `/admin/assets/...` → `admin/theme/default/assets/...`
|
||||
- `/plugins/<name>/assets/...` → `plugins/<name>/assets/...`
|
||||
|
||||
## Admin routing
|
||||
|
||||
Via `public/admin.php`:
|
||||
Admin routing goes through `public/admin.php` with clean URLs:
|
||||
|
||||
```
|
||||
/admin/dashboard → ?route=dashboard
|
||||
/admin/content → ?route=content
|
||||
/admin/plugins → ?route=plugins
|
||||
```
|
||||
|
||||
`cms/router.php` or `.htaccess` converts `/admin/<route>` to `?route=<route>`.
|
||||
|
||||
### Route access control (RBAC)
|
||||
|
||||
Each route is checked via `AdminAuth::hasPermission($route)`:
|
||||
- The `admin` role has wildcard `*` access
|
||||
- Other roles have an explicit list of allowed routes in `ROLE_PERMISSIONS`
|
||||
- Unauthorized routes return a **403 error**
|
||||
|
||||
## Plugin admin routing
|
||||
|
||||
System plugins can register admin routes. These are handled by `PluginManager::handleAdminRoute($route)`:
|
||||
|
||||
1. The system plugin registers routes via `getAdminRoutes()`
|
||||
2. `PluginManager::getAdminMenuItems()` collects admin menu items via `getAdminMenu()`
|
||||
3. On an admin request `PluginManager::handleAdminRoute($route)` finds a plugin that handles the route
|
||||
4. The plugin method `handleAdminRoute($route)` renders the page content
|
||||
|
||||
```php
|
||||
// Routes: /admin/dashboard, /admin/content, etc.
|
||||
// Query parameter: ?route=dashboard
|
||||
// PluginManager calls this for /admin/my-system
|
||||
$plugin->handleAdminRoute('my-system');
|
||||
```
|
||||
@@ -4,28 +4,48 @@ Content is stored in the `content/` folder without a database.
|
||||
|
||||
## Supported file formats
|
||||
|
||||
- `.md` - Markdown (recommended)
|
||||
- `.php` - Dynamic PHP pages
|
||||
- `.html` - Static HTML pages
|
||||
- `.md` — Markdown (recommended)
|
||||
- `.php` — Dynamic PHP pages
|
||||
- `.html` — Static HTML pages
|
||||
|
||||
## File name conventions
|
||||
|
||||
```
|
||||
en.page-name.md # English page
|
||||
nl.pagina-naam.md # Dutch page
|
||||
nl.pagina-naam.md # Dutch page
|
||||
en.page-name.md # English page
|
||||
nl.folder/ # Dutch folder (requires an index file to be clickable)
|
||||
```
|
||||
|
||||
The language prefix is dynamically removed based on available languages via `getAvailableLanguages()`.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
Markdown files can contain frontmatter metadata:
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: full_content
|
||||
plugins: HTMLBlock
|
||||
layout: left_sidebar
|
||||
plugins:
|
||||
- HTMLBlock
|
||||
- Navigation
|
||||
---
|
||||
|
||||
# Page title
|
||||
|
||||
Content...
|
||||
```
|
||||
```
|
||||
|
||||
## Frontmatter fields
|
||||
|
||||
- `layout` — Layout name (must exist in the `theme.json` template mapping; fallback to `config.default_template`)
|
||||
- `plugins` — List of content plugins that appear in the sidebar
|
||||
|
||||
### plugins field
|
||||
|
||||
The `plugins` field determines:
|
||||
- **Which plugins** appear in the sidebar (only content plugins, not system plugins)
|
||||
- **The order** of the plugins in the sidebar (top to bottom)
|
||||
|
||||
The order in the frontmatter is the order in which the plugins are shown. In the admin content-edit page you can adjust the order with up/down buttons; the frontmatter is updated live.
|
||||
|
||||
Plugins are not shown when the chosen layout has no sidebar (e.g. `full_content`).
|
||||
@@ -5,12 +5,44 @@
|
||||
1. Login at `/admin`
|
||||
2. Go to **Content**
|
||||
3. Choose a folder or create a new page
|
||||
4. Edit content in the editor
|
||||
4. Edit content in the CodeMirror editor
|
||||
5. Save with Ctrl+S
|
||||
|
||||
## Editor features
|
||||
|
||||
- **CodeMirror** editor with syntax highlighting
|
||||
- **CodeMirror** editor with syntax highlighting (Markdown, PHP, HTML)
|
||||
- **Toolbar** for quick Markdown insertion
|
||||
- **Live preview** via button
|
||||
- **Auto-save** backups in `.bak/` folder
|
||||
- **Shortcuts**: Ctrl+S (save), Ctrl+N (new)
|
||||
- **Auto-save** backups in `.bak/` folder
|
||||
|
||||
## Layout selection
|
||||
|
||||
On the content-edit page you can choose the layout from the layouts defined in `theme.json` (template mapping). The selected layout is stored in the frontmatter `layout:` key.
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
---
|
||||
```
|
||||
|
||||
Unknown layouts fall back to `config.default_template` from `theme.json`.
|
||||
|
||||
## Plugin selection and order
|
||||
|
||||
On the content-edit page you can choose content plugins for the sidebar:
|
||||
|
||||
- **Selection** — Only content plugins (from `plugin.json` with `type: "content"`) appear in the plugin selection
|
||||
- **Order** — Adjust the order with up/down buttons
|
||||
- **Frontmatter update** — The frontmatter `plugins:` key is updated live on changes
|
||||
- **Layout without sidebar** — Plugins are hidden when the chosen layout has no sidebar (e.g. `full_content`)
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
plugins:
|
||||
- HTMLBlock
|
||||
- Navigation
|
||||
---
|
||||
```
|
||||
|
||||
The order in the frontmatter determines the order of the plugins in the sidebar (top to bottom).
|
||||
@@ -1,3 +1,12 @@
|
||||
# Manual
|
||||
|
||||
Welcome to the CodePress CMS manual (version 2.6.1). CodePress is a file-based CMS without a database — content, configuration and users are stored in files.
|
||||
|
||||
The manual is divided into four sections:
|
||||
|
||||
- **Admin Manager** — Managing the website: dashboard, content, configuration, themes, security, plugins (content and system), users with roles, statistics and logs
|
||||
- **Content Manager** — Managing content: structure, managing pages (layout selection, plugin order), media and the Content API
|
||||
- **Theme Developer** — Developing themes: structure, `theme.json`, Twig templates, SCSS styling (only CSS source), layouts and new themes
|
||||
- **CodePress Developer** — Core development: architecture, core classes, plugin development (content/system), routing, security (RBAC), testing, debugging and performance
|
||||
|
||||
Select a topic from the navigation on the left.
|
||||
@@ -3,7 +3,7 @@
|
||||
The theme developer guide contains the following topics:
|
||||
|
||||
- **Theme structure** - Folder structure of a theme
|
||||
- **theme.json** - Theme configuration
|
||||
- **theme.json** - Configuration of a theme
|
||||
- **Twig templates** - Twig syntax, variables and blocks
|
||||
- **SCSS styling** - Compiling and writing SCSS
|
||||
- **Layouts** - Defining and using layouts
|
||||
|
||||
@@ -2,20 +2,49 @@
|
||||
|
||||
```
|
||||
themes/my-theme/
|
||||
├── theme.json # Theme configuration
|
||||
├── base.twig # Main layout
|
||||
├── full_content.twig # Layout: full-width
|
||||
├── left_sidebar.twig # Layout: left sidebar
|
||||
├── right_sidebar.twig # Layout: right sidebar
|
||||
├── custom.twig # Layout: custom
|
||||
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||
├── base.twig # Main layout (head, header, nav, breadcrumb, footer)
|
||||
├── full_content.twig # Layout: full width
|
||||
├── left_sidebar.twig # Layout: sidebar on the left
|
||||
├── right_sidebar.twig # Layout: sidebar on the right
|
||||
├── custom1.twig # Layout: custom
|
||||
├── guide.twig # Layout: guide with sidebar (Navigation plugin)
|
||||
├── partials/
|
||||
│ ├── header.twig
|
||||
│ ├── navigation.twig
|
||||
│ └── footer.twig
|
||||
└── assets/
|
||||
├── scss/theme.scss # SCSS source
|
||||
├── css/ # CSS files
|
||||
├── js/theme.js # JavaScript
|
||||
├── fonts/ # Fonts
|
||||
└── img/ # Images
|
||||
├── scss/theme.scss # SCSS source (the only CSS source)
|
||||
├── css_compiled/ # Generated by scssphp (read-only, do not edit manually)
|
||||
├── css/ # External CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
|
||||
├── js/ # app.js, bootstrap.bundle.min.js
|
||||
├── fonts/ # bootstrap-icons.woff, woff2
|
||||
└── img/ # favicon, icon, world-map
|
||||
```
|
||||
|
||||
## SCSS is the only CSS source
|
||||
|
||||
- **ALWAYS** edit `assets/scss/theme.scss` — this is the only CSS source
|
||||
- `ThemeManager` compiles SCSS at runtime to `assets/css_compiled/theme.css` via scssphp
|
||||
- `assets/css_compiled/` is **read-only** — do not edit manually
|
||||
- **NEVER** create or edit `assets/css/theme.css` manually; this file must not exist
|
||||
- `ThemeManager::getCssUrl()` priority: 1) `assets/css/theme.css` (manual), 2) `assets/css_compiled/theme.css` (compiled). If `theme.css` exists, the SCSS is ignored.
|
||||
- After SCSS changes: remove `assets/css_compiled/theme.css` and `.mtime` to force recompilation
|
||||
|
||||
```bash
|
||||
rm themes/my-theme/assets/css_compiled/theme.css themes/my-theme/assets/css_compiled/.mtime
|
||||
```
|
||||
|
||||
## guide.twig
|
||||
|
||||
Special layout for guides. Injects the Navigation plugin into the sidebar for sidebar navigation. Automatically used for pages in the `guide/` folder.
|
||||
|
||||
## Plugin CSS loading
|
||||
|
||||
Plugin CSS is automatically loaded after theme CSS in `base.twig`, so themes can override plugin styling:
|
||||
|
||||
```twig
|
||||
{% for cssUrl in plugin_css_urls|default([]) %}
|
||||
<link href="{{ cssUrl }}" rel="stylesheet">
|
||||
{% endfor %}
|
||||
```
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
De admin beheerder handleiding bevat de volgende onderwerpen:
|
||||
|
||||
- **Dashboard** - Overzicht van de website
|
||||
- **Content beheer** - Bestanden en pagina's beheren
|
||||
- **Configuratie** - Site instellingen
|
||||
- **Thema beheer** - Thema's beheren en aanmaken
|
||||
- **Beveiliging** - Bot bescherming en sessies
|
||||
- **Plugins** - Plugins beheren en aanmaken
|
||||
- **Gebruikers** - Gebruikers toevoegen en verwijderen
|
||||
- **Statistieken** - Bezoekersstatistieken bekijken
|
||||
- **Dashboard** - Role-based overzicht van de website (Admin, Content Beheerder, BI Beheerder, Site Admin)
|
||||
- **Content beheer** - Bestanden en pagina's beheren met layout selectie en plugins
|
||||
- **Configuratie** - Site instellingen (titel, taal, auteur, analytics, logging)
|
||||
- **Thema beheer** - Thema's beheren, activeren en aanmaken (SCSS compilatie)
|
||||
- **Beveiliging** - Bot bescherming, rate limiting en sessie instellingen
|
||||
- **Plugins** - Plugins beheren: content (sidebar) en systeem (admin menu/API)
|
||||
- **Gebruikers** - Gebruikersbeheer met rollen (Admin, Content Beheerder, BI Beheerder, Site Admin)
|
||||
- **Statistieken** - Bezoekersstatistieken bekijken en exporteren
|
||||
- **Logs** - Logbestanden bekijken en filteren
|
||||
- **Media** - Media bestanden beheren
|
||||
- **Update** - Controleren op updates
|
||||
|
||||
@@ -2,16 +2,45 @@
|
||||
|
||||
## Bestanden beheren
|
||||
|
||||
- **Uploaden** - Media bestanden uploaden
|
||||
- **Nieuwe map** - Mappen structuur aanmaken
|
||||
- **Nieuw bestand** - Pagina aanmaken
|
||||
- **Bewerken** - Bestaande content wijzigen
|
||||
- **Hernoemen** - Bestandsnamen aanpassen
|
||||
- **Verplaatsen** - Content verplaatsen
|
||||
- **Verwijderen** - Content verwijderen
|
||||
- **Nieuwe map** — Mappen structuur aanmaken
|
||||
- **Nieuw bestand** — Pagina aanmaken (`.md`, `.php`, `.html`)
|
||||
- **Bewerken** — Bestaande content wijzigen in CodeMirror editor
|
||||
- **Hernoemen** — Bestands- of mapnamen aanpassen
|
||||
- **Verplaatsen** — Content verplaatsen naar andere map
|
||||
- **Verwijderen** — Content verwijderen
|
||||
- **Map hernoemen** — Map naam wijzigen
|
||||
|
||||
## Editor
|
||||
## Editor (content-edit)
|
||||
|
||||
- CodeMirror met syntax highlighting
|
||||
- Toolbar voor Markdown formatting
|
||||
- Sneltoetsen: Ctrl+S (opslaan), Ctrl+N (nieuw)
|
||||
- **CodeMirror** met syntax highlighting (Markdown, PHP, HTML)
|
||||
- **Toolbar** voor snel Markdown invoeren
|
||||
- **Sneltoetsen**: Ctrl+S (opslaan), Ctrl+N (nieuw)
|
||||
|
||||
## Layout selectie
|
||||
|
||||
Op de content-edit pagina kun je de layout kiezen uit de layouts gedefinieerd in `theme.json` (template mapping). De geselecteerde layout wordt opgeslagen in de frontmatter `layout:` key.
|
||||
|
||||
## Plugins op pagina's
|
||||
|
||||
- Content plugins (uit `plugin.json` met `type: "content"`) verschijnen in de plugin selectie
|
||||
- Kies welke plugins in de sidebar verschijnen
|
||||
- **Volgorde aanpasbaar** met up/down knoppen
|
||||
- De plugin volgorde wordt opgeslagen in de frontmatter `plugins:` key
|
||||
- Plugins worden **verbergen** als de gekozen layout geen sidebar heeft (bijv. `full_content`)
|
||||
|
||||
## Frontmatter
|
||||
|
||||
De editor werkt de frontmatter live bij bij wijzigingen van layout of plugin selectie:
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
plugins:
|
||||
- HTMLBlock
|
||||
- Navigation
|
||||
---
|
||||
|
||||
# Pagina titel
|
||||
|
||||
Content...
|
||||
```
|
||||
@@ -1,10 +1,32 @@
|
||||
# Dashboard
|
||||
|
||||
Het dashboard toont een overzicht van:
|
||||
Het dashboard toont een **role-based overzicht** van de website. Welke widgets zichtbaar zijn, hangt af van de rol van de ingelogde gebruiker.
|
||||
|
||||
- Weergaven (30 dagen)
|
||||
- Unieke bezoekers
|
||||
## Rollen en widgets
|
||||
|
||||
### Admin
|
||||
- Weergaven (30 dagen) en unieke bezoekers
|
||||
- Aantal pagina's en mappen
|
||||
- Actieve plugins
|
||||
- Recente activiteit
|
||||
- Snelle acties
|
||||
- Systeem informatie
|
||||
- Snelle acties (nieuwe pagina, plugin, thema)
|
||||
|
||||
### Content Beheerder
|
||||
- Aantal pagina's en mappen
|
||||
- Recente content wijzigingen
|
||||
- Snelle acties (nieuwe pagina, map)
|
||||
|
||||
### BI Beheerder
|
||||
- Weergaven (30 dagen) en unieke bezoekers
|
||||
- Top pagina's
|
||||
- Recente bezoeken
|
||||
|
||||
### Site Admin
|
||||
- Actieve plugins en thema's
|
||||
- Systeem informatie
|
||||
- Snelle acties (thema, plugin, update)
|
||||
|
||||
## Toegang
|
||||
|
||||
Het dashboard is de standaard pagina na login (`/admin/dashboard`). Alle rollen hebben toegang tot het dashboard.
|
||||
@@ -1,13 +1,40 @@
|
||||
# Gebruikers
|
||||
|
||||
Gebruikers worden opgeslagen in `admin/config/admin.json` (file-based, geen database). Elke gebruiker heeft een **rol** die bepaalt welke admin routes en sidebar items zichtbaar zijn.
|
||||
|
||||
## Rollen
|
||||
|
||||
CodePress kent vier rollen, gedefinieerd in `AdminAuth::ROLE_PERMISSIONS`:
|
||||
|
||||
| Rol | Label | Permissies |
|
||||
|-----|-------|-----------|
|
||||
| `admin` | Admin | Alles (`*`) |
|
||||
| `content-manager` | Content Beheerder | Content beheer, handleiding |
|
||||
| `bi-manager` | BI Beheerder | Statistieken, logs, handleiding |
|
||||
| `site-admin` | Site Admin | Thema, plugins, statistieken, logs, update, handleiding |
|
||||
|
||||
Rollen worden getoond met hun label via `AdminAuth::ROLE_LABELS`.
|
||||
|
||||
## Gebruiker toevoegen
|
||||
|
||||
1. Ga naar **Gebruikers**
|
||||
2. Vul gebruikersnaam in
|
||||
3. Kies wachtwoord
|
||||
4. Klik **Toevoegen**
|
||||
3. Kies wachtwoord (opgeslagen als bcrypt hash)
|
||||
4. Selecteer een rol
|
||||
5. Klik **Toevoegen**
|
||||
|
||||
## Gebruiker bewerken
|
||||
|
||||
- Wachtwoord wijzigen (nieuwe bcrypt hash)
|
||||
- Rol wijzigen (beïnvloedt direct zichtbare routes en sidebar items)
|
||||
|
||||
## Gebruiker verwijderen
|
||||
|
||||
- Kan niet voor eigen account
|
||||
- Bevestig met wachtwoord
|
||||
- Bevestig met wachtwoord
|
||||
|
||||
## Toegangscontrole
|
||||
|
||||
- Route access wordt gecontroleerd in `public/admin.php` via `AdminAuth::hasPermission()`
|
||||
- Onbevoegde routes geven een **403 error**
|
||||
- Sidebar items worden conditioneel getoond via de `has_permission()` Twig function in `admin.twig`
|
||||
@@ -3,17 +3,64 @@
|
||||
## Thema's beheren
|
||||
|
||||
1. Ga naar **Thema** in admin menu
|
||||
2. **Activeren** - Kies actief thema
|
||||
3. **SCSS compileren** - Verwerk SCSS naar CSS
|
||||
4. **Nieuw thema** - Eigen thema aanmaken
|
||||
2. **Activeren** — Kies actief thema (wordt opgeslagen in `config.json`)
|
||||
3. **SCSS compileren** — Verwerk SCSS naar CSS (geforceerd)
|
||||
4. **Nieuw thema** — Eigen thema aanmaken via admin of handmatig
|
||||
|
||||
## Thema structuur
|
||||
|
||||
```
|
||||
themes/default/
|
||||
├── theme.json # Thema configuratie
|
||||
├── base.twig # Hoofd layout
|
||||
├── full_content.twig # Layouts
|
||||
├── partials/ # Header, nav, footer
|
||||
└── assets/ # CSS, JS, afbeeldingen
|
||||
```
|
||||
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||
├── base.twig # Hoofd layout (head, header, nav, breadcrumb, footer)
|
||||
├── full_content.twig # Layout: volledige breedte
|
||||
├── left_sidebar.twig # Layout: sidebar links
|
||||
├── right_sidebar.twig # Layout: sidebar rechts
|
||||
├── custom1.twig # Layout: custom
|
||||
├── guide.twig # Layout: handleiding met sidebar (Navigation plugin)
|
||||
├── partials/ # header.twig, navigation.twig, footer.twig
|
||||
└── assets/
|
||||
├── scss/theme.scss # SCSS bron (enige CSS bron — handmatige css/theme.css mag niet bestaan)
|
||||
├── css_compiled/ # Gegenereerd door scssphp (read-only, niet handmatig aanpassen)
|
||||
├── css/ # Externe CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
|
||||
├── js/ # app.js, bootstrap.bundle.min.js
|
||||
├── fonts/ # bootstrap-icons.woff, woff2
|
||||
└── img/ # favicon, icon, world-map
|
||||
```
|
||||
|
||||
## theme.json
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "default",
|
||||
"config": {
|
||||
"default_template": "full_content"
|
||||
},
|
||||
"template": {
|
||||
"full_content": "full_content.twig",
|
||||
"left_sidebar": "left_sidebar.twig",
|
||||
"right_sidebar": "right_sidebar.twig",
|
||||
"custom1": "custom1.twig",
|
||||
"guide": "guide.twig"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `title` — Weergavenaam in admin
|
||||
- `config.default_template` — Standaard layout voor pagina's zonder `layout:` frontmatter
|
||||
- `template` — Mapping van layout naam naar `.twig` bestand
|
||||
|
||||
## SCSS compilatie
|
||||
|
||||
- `ThemeManager` compileert `assets/scss/theme.scss` runtime naar `assets/css_compiled/theme.css` via scssphp
|
||||
- `css_compiled/` is **read-only** — niet handmatig aanpassen
|
||||
- `assets/css/theme.css` mag **niet** bestaan; anders negeert `ThemeManager::getCssUrl()` de SCSS
|
||||
- Na SCSS wijzigingen: verwijder `assets/css_compiled/theme.css` en `.mtime` om te forceren
|
||||
|
||||
## Layouts
|
||||
|
||||
Layouts worden gekozen via frontmatter `layout:` key in content bestanden. Onbekende layouts vallen terug op `config.default_template` uit `theme.json`.
|
||||
|
||||
## guide.twig
|
||||
|
||||
Speciale layout voor handleidingen. Injecteert de Navigation plugin in de sidebar voor zijbalk navigatie. Wordt automatisch gebruikt voor pagina's in de `guide/` map.
|
||||
@@ -2,13 +2,20 @@
|
||||
|
||||
De CodePress developer handleiding bevat de volgende onderwerpen:
|
||||
|
||||
- **Architectuur** - CMS architectuur en mappenstructuur
|
||||
- **Core classes** - CodePressCMS, ThemeManager, PluginManager
|
||||
- **Plugin development** - Plugins ontwikkelen
|
||||
- **Routing** - Frontend en admin routing
|
||||
- **Beveiliging** - XSS, CSRF, path traversal
|
||||
- **Testing** - Penetratie, accessibility en functionele tests
|
||||
- **Debugging** - Logging en cache
|
||||
- **Performance** - OPcache en SCSS caching
|
||||
- **Architectuur** — CMS architectuur en mappenstructuur
|
||||
- **Core classes** — CodePressCMS, ThemeManager, PluginManager, AdminAuth
|
||||
- **Plugin development** — Plugin types (content/systeem), structuur, API
|
||||
- **Routing** — Frontend, admin en plugin admin routing
|
||||
- **Beveiliging** — XSS, CSRF, path traversal, RBAC
|
||||
- **Testing** — Penetratie, accessibility en functionele tests
|
||||
- **Debugging** — Logging en cache
|
||||
- **Performance** — OPcache en SCSS caching
|
||||
|
||||
Belangrijke concepten in CodePress 2.6.1:
|
||||
|
||||
- **Plugin types** — Content plugins (sidebar) en systeem plugins (admin menu, routes, API)
|
||||
- **Admin plugin API** — Systeem plugins kunnen admin menu items en routes registreren
|
||||
- **SCSS compilatie** — `ThemeManager` compileert SCSS naar `css_compiled/` via scssphp (read-only)
|
||||
- **Asset serving** — `public/asset.php` serveert themes/, admin/assets/ en plugins/ op Apache (i.p.v. PHP dev router)
|
||||
|
||||
Selecteer een onderwerp uit de navigatie aan de linkerkant.
|
||||
@@ -11,7 +11,7 @@ codepress/
|
||||
│ ├── config/ # app.php, admin.json
|
||||
│ ├── src/ # AdminAuth
|
||||
│ └── theme/default/views/ # Twig templates
|
||||
├── themes/ # Thema's (default, demo, ...)
|
||||
├── themes/ # Thema's (default, ...)
|
||||
├── plugins/ # Plugins (Dashboard, HTMLBlock, Navigation, ...)
|
||||
├── content/ # Content bestanden (.md, .php, .html)
|
||||
├── language/ # Taalbestanden (nl/, en/, de/)
|
||||
|
||||
@@ -2,18 +2,60 @@
|
||||
|
||||
## Frontend routing
|
||||
|
||||
Via `cms/router.php` voor PHP dev server:
|
||||
Frontend routing verloopt via `cms/router.php` (PHP dev server) of `.htaccess` (Apache). Beide zorgen voor clean URLs.
|
||||
|
||||
```php
|
||||
// Schone URLs: /nl/pagina
|
||||
// Query: ?page=pagina&lang=nl
|
||||
### PHP dev server
|
||||
|
||||
Start de server met:
|
||||
|
||||
```bash
|
||||
php -S localhost:8080 cms/router.php
|
||||
```
|
||||
|
||||
`cms/router.php` serveert:
|
||||
- Schone URLs: `/nl/pagina` → `public/index.php?page=pagina&lang=nl`
|
||||
- `themes/` assets
|
||||
- `admin/assets/` assets
|
||||
- `plugins/` assets
|
||||
|
||||
### Apache (live server)
|
||||
|
||||
`public/.htaccess` herschrijft URLs naar `public/index.php`. Asset URLs (`/themes/`, `/admin/assets/`, `/plugins/`) worden doorgestuurd naar `public/asset.php`.
|
||||
|
||||
`public/asset.php` serveert bestanden vanuit de juiste mappen met het juiste MIME-type:
|
||||
- `/themes/<name>/assets/...` → `themes/<name>/assets/...`
|
||||
- `/admin/assets/...` → `admin/theme/default/assets/...`
|
||||
- `/plugins/<name>/assets/...` → `plugins/<name>/assets/...`
|
||||
|
||||
## Admin routing
|
||||
|
||||
Via `public/admin.php`:
|
||||
Admin routing verloopt via `public/admin.php` met clean URLs:
|
||||
|
||||
```
|
||||
/admin/dashboard → ?route=dashboard
|
||||
/admin/content → ?route=content
|
||||
/admin/plugins → ?route=plugins
|
||||
```
|
||||
|
||||
`cms/router.php` of `.htaccess` zet `/admin/<route>` om naar `?route=<route>`.
|
||||
|
||||
### Route access control (RBAC)
|
||||
|
||||
Elke route wordt gecontroleerd via `AdminAuth::hasPermission($route)`:
|
||||
- `admin` rol heeft wildcard `*` toegang
|
||||
- Andere rollen hebben een expliciete lijst van toegestane routes in `ROLE_PERMISSIONS`
|
||||
- Onbevoegde routes geven een **403 error**
|
||||
|
||||
## Plugin admin routing
|
||||
|
||||
Systeem plugins kunnen admin routes registreren. Deze worden afgehandeld door `PluginManager::handleAdminRoute($route)`:
|
||||
|
||||
1. Systeem plugin registreert routes via `getAdminRoutes()`
|
||||
2. `PluginManager::getAdminMenuItems()` verzamelt admin menu items via `getAdminMenu()`
|
||||
3. Bij een admin request zoekt `PluginManager::handleAdminRoute($route)` een plugin die de route afhandelt
|
||||
4. De plugin methode `handleAdminRoute($route)` rendert de pagina inhoud
|
||||
|
||||
```php
|
||||
// Routes: /admin/dashboard, /admin/content, etc.
|
||||
// Query parameter: ?route=dashboard
|
||||
// PluginManager roept deze aan voor /admin/mijn-systeem
|
||||
$plugin->handleAdminRoute('mijn-systeem');
|
||||
```
|
||||
@@ -4,28 +4,48 @@ Content wordt opgeslagen in de `content/` map zonder database.
|
||||
|
||||
## Ondersteunde bestandsformaten
|
||||
|
||||
- `.md` - Markdown (aanbevolen)
|
||||
- `.php` - Dynamische PHP pagina's
|
||||
- `.html` - Statische HTML pagina's
|
||||
- `.md` — Markdown (aanbevolen)
|
||||
- `.php` — Dynamische PHP pagina's
|
||||
- `.html` — Statische HTML pagina's
|
||||
|
||||
## Bestandsnaam conventies
|
||||
|
||||
```
|
||||
nl.pagina-naam.md # Nederlandse pagina
|
||||
en.page-name.md # Engelse pagina
|
||||
nl.map/ # Nederlandse map (vereist index bestand om klikbaar te zijn)
|
||||
```
|
||||
|
||||
Taalprefix wordt dynamisch verwijderd op basis van beschikbare talen via `getAvailableLanguages()`.
|
||||
|
||||
## Frontmatter
|
||||
|
||||
Markdown bestanden kunnen frontmatter metadata bevatten:
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: full_content
|
||||
plugins: HTMLBlock
|
||||
layout: left_sidebar
|
||||
plugins:
|
||||
- HTMLBlock
|
||||
- Navigation
|
||||
---
|
||||
|
||||
# Pagina titel
|
||||
|
||||
Content...
|
||||
```
|
||||
```
|
||||
|
||||
## Frontmatter velden
|
||||
|
||||
- `layout` — Layout naam (moet voorkomen in `theme.json` template mapping; fallback op `config.default_template`)
|
||||
- `plugins` — Lijst van content plugins die in de sidebar verschijnen
|
||||
|
||||
### plugins veld
|
||||
|
||||
Het `plugins` veld bepaalt:
|
||||
- **Welke plugins** in de sidebar verschijnen (alleen content plugins, niet systeem plugins)
|
||||
- **De volgorde** van de plugins in de sidebar (top tot onder)
|
||||
|
||||
De volgorde in de frontmatter is de volgorde waarin de plugins worden getoond. In de admin content-edit pagina kun je de volgorde aanpassen met up/down knoppen; de frontmatter wordt live bijgewerkt.
|
||||
|
||||
Plugins worden niet getoond als de gekozen layout geen sidebar heeft (bijv. `full_content`).
|
||||
@@ -5,12 +5,44 @@
|
||||
1. Login op `/admin`
|
||||
2. Ga naar **Content**
|
||||
3. Kies een map of maak een nieuwe pagina
|
||||
4. Bewerk content in de editor
|
||||
4. Bewerk content in de CodeMirror editor
|
||||
5. Sla op met Ctrl+S
|
||||
|
||||
## Editor functies
|
||||
|
||||
- **CodeMirror** editor met syntax highlighting
|
||||
- **CodeMirror** editor met syntax highlighting (Markdown, PHP, HTML)
|
||||
- **Toolbar** voor snel Markdown invoegen
|
||||
- **Live preview** via knop
|
||||
- **Auto-save** backups in `.bak/` map
|
||||
- **Sneltoetsen**: Ctrl+S (opslaan), Ctrl+N (nieuw)
|
||||
- **Auto-save** backups in `.bak/` map
|
||||
|
||||
## Layout selectie
|
||||
|
||||
Op de content-edit pagina kun je de layout kiezen uit de layouts gedefinieerd in `theme.json` (template mapping). De geselecteerde layout wordt opgeslagen in de frontmatter `layout:` key.
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
---
|
||||
```
|
||||
|
||||
Onbekende layouts vallen terug op `config.default_template` uit `theme.json`.
|
||||
|
||||
## Plugin selectie en volgorde
|
||||
|
||||
Op de content-edit pagina kun je content plugins kiezen voor de sidebar:
|
||||
|
||||
- **Selectie** — Alleen content plugins (uit `plugin.json` met `type: "content"`) verschijnen in de plugin selectie
|
||||
- **Volgorde** — Pas de volgorde aan met up/down knoppen
|
||||
- **Frontmatter update** — De frontmatter `plugins:` key wordt live bijgewerkt bij wijzigingen
|
||||
- **Layout zonder sidebar** — Plugins worden verborgen als de gekozen layout geen sidebar heeft (bijv. `full_content`)
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
plugins:
|
||||
- HTMLBlock
|
||||
- Navigation
|
||||
---
|
||||
```
|
||||
|
||||
De volgorde in de frontmatter bepaalt de volgorde van de plugins in de sidebar (top tot onder).
|
||||
@@ -1,3 +1,12 @@
|
||||
# Handleiding
|
||||
|
||||
Welkom bij de CodePress CMS handleiding (versie 2.6.1). CodePress is een file-based CMS zonder database — content, configuratie en gebruikers worden opgeslagen in bestanden.
|
||||
|
||||
De handleiding is verdeeld in vier secties:
|
||||
|
||||
- **Admin Beheerder** — Beheer van de website: dashboard, content, configuratie, thema's, beveiliging, plugins (content en systeem), gebruikers met rollen, statistieken en logs
|
||||
- **Content Beheerder** — Content beheren: structuur, pagina's beheren (layout selectie, plugin volgorde), media en de Content API
|
||||
- **Theme Developer** — Thema's ontwikkelen: structuur, `theme.json`, Twig templates, SCSS styling (enige CSS bron), layouts en nieuwe thema's
|
||||
- **CodePress Developer** — Core ontwikkeling: architectuur, core classes, plugin development (content/systeem), routing, beveiliging (RBAC), testing, debugging en performance
|
||||
|
||||
Selecteer een onderwerp uit de navigatie aan de linkerkant.
|
||||
@@ -2,20 +2,49 @@
|
||||
|
||||
```
|
||||
themes/mijn-thema/
|
||||
├── theme.json # Thema configuratie
|
||||
├── base.twig # Hoofd layout
|
||||
├── full_content.twig # Layout: full-width
|
||||
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||
├── base.twig # Hoofd layout (head, header, nav, breadcrumb, footer)
|
||||
├── full_content.twig # Layout: volledige breedte
|
||||
├── left_sidebar.twig # Layout: sidebar links
|
||||
├── right_sidebar.twig # Layout: sidebar rechts
|
||||
├── custom.twig # Layout: custom
|
||||
├── custom1.twig # Layout: custom
|
||||
├── guide.twig # Layout: handleiding met sidebar (Navigation plugin)
|
||||
├── partials/
|
||||
│ ├── header.twig
|
||||
│ ├── navigation.twig
|
||||
│ └── footer.twig
|
||||
└── assets/
|
||||
├── scss/theme.scss # SCSS bron
|
||||
├── css/ # CSS bestanden
|
||||
├── js/theme.js # JavaScript
|
||||
├── fonts/ # Lettertypes
|
||||
└── img/ # Afbeeldingen
|
||||
├── scss/theme.scss # SCSS bron (de enige CSS bron)
|
||||
├── css_compiled/ # Gegenereerd door scssphp (read-only, niet handmatig aanpassen)
|
||||
├── css/ # Externe CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
|
||||
├── js/ # app.js, bootstrap.bundle.min.js
|
||||
├── fonts/ # bootstrap-icons.woff, woff2
|
||||
└── img/ # favicon, icon, world-map
|
||||
```
|
||||
|
||||
## SCSS is de enige CSS bron
|
||||
|
||||
- **ALTIJD** `assets/scss/theme.scss` aanpassen — dit is de enige CSS bron
|
||||
- `ThemeManager` compileert SCSS runtime naar `assets/css_compiled/theme.css` via scssphp
|
||||
- `assets/css_compiled/` is **read-only** — niet handmatig aanpassen
|
||||
- **NOOIT** `assets/css/theme.css` handmatig aanmaken of aanpassen; dit bestand mag niet bestaan
|
||||
- `ThemeManager::getCssUrl()` prioriteit: 1) `assets/css/theme.css` (handmatig), 2) `assets/css_compiled/theme.css` (gecompileerd). Als `theme.css` bestaat, wordt de SCSS negeren.
|
||||
- Na SCSS wijzigingen: verwijder `assets/css_compiled/theme.css` en `.mtime` om te forceren
|
||||
|
||||
```bash
|
||||
rm themes/mijn-thema/assets/css_compiled/theme.css themes/mijn-thema/assets/css_compiled/.mtime
|
||||
```
|
||||
|
||||
## guide.twig
|
||||
|
||||
Speciale layout voor handleidingen. Injecteert de Navigation plugin in de sidebar voor zijbalk navigatie. Wordt automatisch gebruikt voor pagina's in de `guide/` map.
|
||||
|
||||
## Plugin CSS laden
|
||||
|
||||
Plugin CSS wordt automatisch geladen ná theme CSS in `base.twig`, zodat thema's plugin styling kunnen overschrijven:
|
||||
|
||||
```twig
|
||||
{% for cssUrl in plugin_css_urls|default([]) %}
|
||||
<link href="{{ cssUrl }}" rel="stylesheet">
|
||||
{% endfor %}
|
||||
```
|
||||
@@ -35,4 +35,14 @@ return [
|
||||
'plugin_development' => 'Plugin Entwicklung',
|
||||
'template_system' => 'Template System',
|
||||
'go_to' => 'Gehe zu',
|
||||
'welcome_title' => 'Willkommen bei CodePress CMS',
|
||||
'welcome_new_install' => 'Neue Installation',
|
||||
'welcome_intro' => 'Sie sehen diese Seite, weil das Inhaltsverzeichnis noch leer ist. Dies ist eine neue Installation von CodePress CMS.',
|
||||
'welcome_next_steps' => 'Nächste Schritte',
|
||||
'welcome_step_1' => 'Melden Sie sich über die Admin-Konsole an, um Inhalte zu verwalten',
|
||||
'welcome_step_2' => 'Fügen Sie Markdown-, PHP- oder HTML-Dateien zum Inhaltsverzeichnis hinzu',
|
||||
'welcome_step_3' => 'Lesen Sie das Handbuch für die vollständige Dokumentation',
|
||||
'welcome_admin_link' => 'Zur Admin-Konsole',
|
||||
'welcome_guide_link' => 'Handbuch ansehen',
|
||||
'welcome_content_empty' => 'Das Inhaltsverzeichnis ist leer',
|
||||
];
|
||||
@@ -35,4 +35,14 @@ return [
|
||||
'plugin_development' => 'Plugin Development',
|
||||
'template_system' => 'Template System',
|
||||
'go_to' => 'Go to',
|
||||
'welcome_title' => 'Welcome to CodePress CMS',
|
||||
'welcome_new_install' => 'New installation',
|
||||
'welcome_intro' => 'You are seeing this page because the content directory is still empty. This is a new installation of CodePress CMS.',
|
||||
'welcome_next_steps' => 'Next steps',
|
||||
'welcome_step_1' => 'Log in via the admin console to manage content',
|
||||
'welcome_step_2' => 'Add Markdown, PHP or HTML files to the content directory',
|
||||
'welcome_step_3' => 'Read the manual for full documentation',
|
||||
'welcome_admin_link' => 'Go to admin console',
|
||||
'welcome_guide_link' => 'View manual',
|
||||
'welcome_content_empty' => 'The content directory is empty',
|
||||
];
|
||||
@@ -35,4 +35,14 @@ return [
|
||||
'plugin_development' => 'Plugin Ontwikkeling',
|
||||
'template_system' => 'Template Systeem',
|
||||
'go_to' => 'Ga naar',
|
||||
'welcome_title' => 'Welkom bij CodePress CMS',
|
||||
'welcome_new_install' => 'Nieuwe installatie',
|
||||
'welcome_intro' => 'U ziet deze pagina omdat de content-map nog leeg is. Dit is een nieuwe installatie van CodePress CMS.',
|
||||
'welcome_next_steps' => 'Volgende stappen',
|
||||
'welcome_step_1' => 'Log in via de admin console om content te beheren',
|
||||
'welcome_step_2' => 'Voeg Markdown-, PHP- of HTML-bestanden toe in de content-map',
|
||||
'welcome_step_3' => 'Bekijk de handleiding voor uitgebreide documentatie',
|
||||
'welcome_admin_link' => 'Naar de admin console',
|
||||
'welcome_guide_link' => 'Handleiding bekijken',
|
||||
'welcome_content_empty' => 'De content-map is leeg',
|
||||
];
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "codepress",
|
||||
"version": "1.7.1",
|
||||
"description": "A lightweight, file-based Content Management System built with PHP and Bootstrap.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build:css": "npx sass --load-path=node_modules src/scss/main.scss public/assets/css/style.css --style=compressed",
|
||||
"watch:css": "npx sass --load-path=node_modules --watch src/scss/main.scss public/assets/css/style.css",
|
||||
"build": "npm run build:css",
|
||||
"clean": "rm -rf node_modules package-lock.json",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.noorlander.info/E.Noorlander/CodePress.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.8",
|
||||
"sass": "^1.94.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
class GeoIPInfo
|
||||
{
|
||||
private ?CMSAPI $api = null;
|
||||
|
||||
public function setAPI(CMSAPI $api): void
|
||||
{
|
||||
$this->api = $api;
|
||||
}
|
||||
|
||||
public function getConfig(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'GeoIP Info',
|
||||
'type' => 'system',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register admin menu items.
|
||||
*/
|
||||
public function getAdminMenu(): array
|
||||
{
|
||||
return [
|
||||
['label' => 'GeoIP Info', 'route' => 'geoip-info', 'icon' => 'bi-globe2'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register admin routes this plugin handles.
|
||||
*/
|
||||
public function getAdminRoutes(): array
|
||||
{
|
||||
return ['geoip-info'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle admin route — render the admin page.
|
||||
*/
|
||||
public function handleAdminRoute(string $route): void
|
||||
{
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
$geoip = new GeoIP();
|
||||
$countryCode = $geoip->lookupCountry($ip);
|
||||
$country = GeoIP::getCountryName($countryCode);
|
||||
$flag = GeoIP::getCountryFlagEmoji($countryCode);
|
||||
|
||||
echo '<h2 class="mb-4"><i class="bi bi-globe2"></i> GeoIP Info</h2>';
|
||||
echo '<div class="card shadow-sm"><div class="card-body">';
|
||||
echo '<table class="table table-sm">';
|
||||
echo '<tr><td>IP adres</td><td><code>' . htmlspecialchars($ip) . '</code></td></tr>';
|
||||
echo '<tr><td>Land</td><td>' . $flag . ' ' . htmlspecialchars($country) . '</td></tr>';
|
||||
echo '</table>';
|
||||
echo '</div></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "GeoIPInfo",
|
||||
"version": "1.0.0",
|
||||
"author": "CodePress",
|
||||
"description": "Systeem plugin voor GeoIP informatie in admin",
|
||||
"type": "system"
|
||||
}
|
||||
@@ -23,6 +23,8 @@ class HTMLBlock
|
||||
$currentPage = $this->api ? $this->api->getCurrentPageTitle() : 'Onbekend';
|
||||
$isHomepage = $this->api ? $this->api->isHomepage() : false;
|
||||
$currentLang = $this->api ? $this->api->getCurrentLanguage() : 'nl';
|
||||
$currentPath = $_GET['page'] ?? '';
|
||||
$currentPath = preg_replace('/\.(md|php|html)$/', '', $currentPath);
|
||||
|
||||
$content = '
|
||||
<p class="mb-2"><strong>Huidige pagina:</strong> ' . htmlspecialchars($currentPage) . '</p>
|
||||
@@ -43,7 +45,7 @@ class HTMLBlock
|
||||
</div>';
|
||||
}
|
||||
|
||||
// Add quick navigation
|
||||
// Add dynamic quick navigation
|
||||
$menu = $this->api->getMenu();
|
||||
if (!empty($menu)) {
|
||||
$content .= '
|
||||
@@ -52,8 +54,25 @@ class HTMLBlock
|
||||
|
||||
foreach ($menu as $item) {
|
||||
if ($item['type'] === 'file') {
|
||||
$url = $this->api->createUrl($item['path']);
|
||||
$content .= '<li><a href="' . htmlspecialchars($url) . '" class="text-decoration-none">📄 ' . htmlspecialchars($item['title']) . '</a></li>';
|
||||
$url = '/' . $currentLang . '/' . $item['path'];
|
||||
$isActive = ($item['path'] === $currentPath) ? ' fw-bold' : '';
|
||||
$content .= '<li><a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" class="text-decoration-none' . $isActive . '"><i class="bi bi-file-earmark"></i> ' . htmlspecialchars($item['title'], ENT_QUOTES, 'UTF-8') . '</a></li>';
|
||||
} elseif ($item['type'] === 'folder' && !empty($item['children'])) {
|
||||
$url = '/' . $currentLang . '/' . $item['path'];
|
||||
$isActive = strpos($currentPath, $item['path']) === 0 ? ' fw-bold' : '';
|
||||
$content .= '<li><a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" class="text-decoration-none' . $isActive . '"><i class="bi bi-folder"></i> ' . htmlspecialchars($item['title'], ENT_QUOTES, 'UTF-8') . '</a></li>';
|
||||
// Show children if current page is in this folder
|
||||
if (strpos($currentPath, $item['path']) === 0) {
|
||||
$content .= '<ul class="list-unstyled ms-3 mb-1">';
|
||||
foreach ($item['children'] as $child) {
|
||||
if ($child['type'] === 'file') {
|
||||
$childUrl = '/' . $currentLang . '/' . $child['path'];
|
||||
$childActive = ($child['path'] === $currentPath) ? ' fw-bold' : '';
|
||||
$content .= '<li><a href="' . htmlspecialchars($childUrl, ENT_QUOTES, 'UTF-8') . '" class="text-decoration-none' . $childActive . '"><i class="bi bi-file-earmark"></i> ' . htmlspecialchars($child['title'], ENT_QUOTES, 'UTF-8') . '</a></li>';
|
||||
}
|
||||
}
|
||||
$content .= '</ul>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,16 @@ Options -Indexes
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# Admin routes: /admin/login → admin.php?route=login
|
||||
# Serve theme assets via asset.php (themes are outside webroot)
|
||||
RewriteRule ^themes/([^/]+)/assets/(.+)$ asset.php [L,QSA]
|
||||
|
||||
# Serve admin theme assets via asset.php
|
||||
RewriteRule ^admin/assets/(.+)$ asset.php [L,QSA]
|
||||
|
||||
# Serve plugin assets via asset.php
|
||||
RewriteRule ^plugins/([^/]+)/assets/(.+)$ asset.php [L,QSA]
|
||||
|
||||
# Admin routes: /admin/login -> admin.php?route=login
|
||||
RewriteRule ^admin$ admin.php?route=dashboard [L,QSA]
|
||||
RewriteRule ^admin/(.+)$ admin.php?route=$1 [L,QSA]
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/**
|
||||
* Asset server - serves static files from themes/, admin/theme/, and plugins/
|
||||
* This file is used on production servers where the PHP router is not available.
|
||||
* The .htaccess rewrites /themes/, /admin/assets/, and /plugins/ URLs to this file.
|
||||
*/
|
||||
|
||||
$basePath = dirname(__DIR__);
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
$path = parse_url($requestUri, PHP_URL_PATH) ?? '/';
|
||||
|
||||
$mimeTypes = [
|
||||
'css' => 'text/css',
|
||||
'js' => 'application/javascript',
|
||||
'svg' => 'image/svg+xml',
|
||||
'png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'ico' => 'image/x-icon',
|
||||
'woff' => 'font/woff',
|
||||
'woff2' => 'font/woff2',
|
||||
'json' => 'application/json',
|
||||
'map' => 'application/json',
|
||||
];
|
||||
|
||||
// Determine which asset directory to serve from
|
||||
$assetFile = null;
|
||||
|
||||
// /themes/<name>/assets/...
|
||||
if (preg_match('#^/themes/([^/]+)/assets/(.+)$#', $path, $m)) {
|
||||
$themeName = $m[1];
|
||||
$assetRel = $m[2];
|
||||
$assetFile = $basePath . '/themes/' . $themeName . '/assets/' . $assetRel;
|
||||
$realBase = realpath($basePath . '/themes/' . $themeName . '/assets');
|
||||
}
|
||||
// /admin/assets/...
|
||||
elseif (preg_match('#^/admin/assets/(.+)$#', $path, $m)) {
|
||||
$assetRel = $m[1];
|
||||
$assetFile = $basePath . '/admin/theme/default/assets/' . $assetRel;
|
||||
$realBase = realpath($basePath . '/admin/theme/default/assets');
|
||||
}
|
||||
// /plugins/<name>/assets/...
|
||||
elseif (preg_match('#^/plugins/([^/]+)/assets/(.+)$#', $path, $m)) {
|
||||
$pluginName = $m[1];
|
||||
$assetRel = $m[2];
|
||||
$assetFile = $basePath . '/plugins/' . $pluginName . '/assets/' . $assetRel;
|
||||
$realBase = realpath($basePath . '/plugins/' . $pluginName . '/assets');
|
||||
}
|
||||
|
||||
if ($assetFile && $realBase) {
|
||||
$realFile = realpath($assetFile);
|
||||
if ($realFile && strpos($realFile, $realBase) === 0 && is_file($realFile)) {
|
||||
$ext = strtolower(pathinfo($realFile, PATHINFO_EXTENSION));
|
||||
if (isset($mimeTypes[$ext])) {
|
||||
header('Content-Type: ' . $mimeTypes[$ext]);
|
||||
}
|
||||
header('Cache-Control: public, max-age=86400');
|
||||
readfile($realFile);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
http_response_code(404);
|
||||
header('Content-Type: text/plain');
|
||||
echo '404 Not Found';
|
||||
@@ -1,361 +0,0 @@
|
||||
// Custom variables (must come before Bootstrap import)
|
||||
$primary: #0d6efd;
|
||||
$navigation-opacity: 0.5; // 50% opacity
|
||||
|
||||
// Import Bootstrap
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
|
||||
// Custom navigation background with 50% opacity of primary color
|
||||
.navigation-50-opacity {
|
||||
background-color: rgba($primary, $navigation-opacity) !important;
|
||||
}
|
||||
|
||||
// Custom CodePress styles
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header.navbar {
|
||||
flex-shrink: 0;
|
||||
height: 66px;
|
||||
}
|
||||
|
||||
/* Navigation section */
|
||||
.navigation-section {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 0.5rem 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Tab styling */
|
||||
.nav-tabs {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
border-radius: 0; // Geen ronde hoeken
|
||||
color: #6c757d;
|
||||
background-color: transparent; // Transparante achtergrond voor eerste laag
|
||||
margin-right: 0.25rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover {
|
||||
color: #495057;
|
||||
background-color: rgba(0, 0, 0, 0.05); // Subtiel hover effect
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #495057;
|
||||
background-color: #ffffff; // Alleen active heeft witte achtergrond
|
||||
border-color: #dee2e6 #dee2e6 transparent;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Dropdown in tabs - eerste laag */
|
||||
.nav-tabs .nav-item.dropdown .nav-link {
|
||||
background-color: transparent; // Transparante achtergrond
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0; // Geen ronde hoeken
|
||||
}
|
||||
|
||||
.nav-tabs .nav-item.dropdown .nav-link:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05); // Subtiel hover effect
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* Dropdown menu styling */
|
||||
.nav-tabs .dropdown-menu {
|
||||
margin-top: 0.25rem;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Dropdown items styling */
|
||||
.nav-tabs .dropdown-menu .dropdown-item {
|
||||
white-space: normal;
|
||||
padding: 0.5rem 1rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Long text truncation for very long titles */
|
||||
.nav-tabs .dropdown-menu .dropdown-item {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
/* Breadcrumb section */
|
||||
.breadcrumb-section {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Breadcrumb styling consistency */
|
||||
.breadcrumb-section .breadcrumb {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-section .breadcrumb-item {
|
||||
color: #6c757d !important;
|
||||
}
|
||||
|
||||
.breadcrumb-section .breadcrumb-item a {
|
||||
color: #0d6efd !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb-section .breadcrumb-item a:hover {
|
||||
color: #0a58ca !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-section .breadcrumb-item.active {
|
||||
color: #6c757d !important;
|
||||
}
|
||||
|
||||
/* Main content area */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
/* Content specific styling */
|
||||
.markdown-content,
|
||||
.php-content,
|
||||
.html-content {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.markdown-content .content-body,
|
||||
.php-content .content-body,
|
||||
.html-content .content-body {
|
||||
padding: 2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Navigation dropdown styling */
|
||||
.navigation-section .btn {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.navigation-section .collapse .nav-link {
|
||||
color: #495057;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.navigation-section .collapse .nav-link:hover {
|
||||
background-color: #e9ecef;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.navigation-section .collapse .nav-link.active {
|
||||
background-color: #0d6efd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.navigation-section .collapse .nav-link i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* File info styling */
|
||||
.file-info {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-info i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: #6c757d;
|
||||
max-width: 250px;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.page-title:hover {
|
||||
color: #495057;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
color: #6c757d;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.site-info a {
|
||||
color: #0d6efd;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.guide-link {
|
||||
color: #6c757d !important;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.guide-link:hover {
|
||||
color: #0d6efd !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.guide-link i {
|
||||
font-size: 1rem;
|
||||
color: #6c757d !important;
|
||||
}
|
||||
|
||||
.guide-link:hover i {
|
||||
color: #0d6efd !important;
|
||||
}
|
||||
|
||||
.auto-link {
|
||||
color: #0d6efd;
|
||||
text-decoration: none;
|
||||
border-bottom: 2px dashed #0d6efd;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.auto-link:hover {
|
||||
color: #0a58ca;
|
||||
text-decoration: none;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #0a58ca;
|
||||
}
|
||||
|
||||
/* Search form */
|
||||
.search-form {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
/* Search input text color - override navbar form-control styling */
|
||||
.navbar .form-control.search-input {
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.navbar .form-control.search-input::placeholder {
|
||||
color: #6c757d !important;
|
||||
}
|
||||
|
||||
/* Card styling */
|
||||
.card-title a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.card-title a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Dropdown submenu styling */
|
||||
.dropdown-menu .dropdown-menu {
|
||||
top: 0;
|
||||
left: 100%;
|
||||
margin-top: -1px;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
/* Show nested dropdowns on hover */
|
||||
.dropdown-submenu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-submenu .dropdown-menu {
|
||||
top: 0;
|
||||
left: 100%;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.dropdown-submenu:hover > .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* For touch devices - click to open */
|
||||
.dropdown-submenu > .dropdown-toggle:active::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding-top: 120px; /* More space for mobile */
|
||||
}
|
||||
|
||||
.markdown-content .content-body,
|
||||
.php-content .content-body,
|
||||
.html-content .content-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-tabs .dropdown-menu {
|
||||
min-width: 180px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.nav-tabs .dropdown-menu .dropdown-item {
|
||||
max-width: 230px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.markdown-content .content-body,
|
||||
.php-content .content-body,
|
||||
.html-content .content-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.nav-tabs .dropdown-menu {
|
||||
min-width: 160px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.nav-tabs .dropdown-menu .dropdown-item {
|
||||
max-width: 180px;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
#site-header .search-input {
|
||||
background-color: #ffffff !important;
|
||||
color: #212529 !important;
|
||||
border: 2px solid #0a369d !important;
|
||||
}
|
||||
|
||||
#site-header .search-input::placeholder {
|
||||
color: #6c757d !important;
|
||||
}
|
||||
|
||||
#site-header .search-input:focus {
|
||||
background-color: #ffffff !important;
|
||||
color: #212529 !important;
|
||||
border-color: #2754b4 !important;
|
||||
box-shadow: 0 0 0 0.2rem rgba(10, 54, 157, 0.25) !important;
|
||||
}
|
||||
|
||||
:root {
|
||||
--header-bg: #0a369d;
|
||||
--header-font: #ffffff;
|
||||
--header-height: 56px;
|
||||
--nav-bg: #2754b4;
|
||||
--nav-font: #ffffff;
|
||||
--nav-height: 42px;
|
||||
--sidebar-bg: #f8f9fa;
|
||||
--sidebar-border: #dee2e6;
|
||||
}
|
||||
|
||||
#site-header {
|
||||
background-image: none;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: var(--header-bg) !important;
|
||||
min-height: var(--header-height);
|
||||
}
|
||||
.navbar .navbar-brand,
|
||||
.navbar .navbar-text,
|
||||
.navbar .btn {
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
.navbar .form-control {
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
.navbar .form-control::placeholder {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
.navbar .search-input {
|
||||
background-color: #ffffff !important;
|
||||
color: #212529 !important;
|
||||
border: 2px solid #0a369d !important;
|
||||
}
|
||||
.navbar .search-input::placeholder {
|
||||
color: #6c757d !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.navbar .search-input:focus {
|
||||
background-color: #ffffff !important;
|
||||
color: #212529 !important;
|
||||
border-color: #2754b4 !important;
|
||||
box-shadow: 0 0 0 0.2rem rgba(10, 54, 157, 0.25) !important;
|
||||
}
|
||||
.navbar .btn-outline-light {
|
||||
border-color: var(--header-font) !important;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background-color: var(--header-bg) !important;
|
||||
border: 1px solid var(--header-font) !important;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
.dropdown-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.btn-outline-light {
|
||||
color: var(--header-font) !important;
|
||||
border-color: var(--header-font) !important;
|
||||
}
|
||||
.btn-outline-light:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
.btn-outline-light:focus, .btn-outline-light:active, .show > .btn-outline-light.dropdown-toggle {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--header-font) !important;
|
||||
border-color: var(--header-font) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.bi-chevron-down {
|
||||
font-size: 0.75em;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
content: "" !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
--bs-breadcrumb-divider: "";
|
||||
--bs-breadcrumb-margin-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.breadcrumb .breadcrumb-item {
|
||||
color: var(--header-bg) !important;
|
||||
}
|
||||
.breadcrumb .breadcrumb-item a {
|
||||
color: var(--header-bg) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
.breadcrumb .breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.sidebar-toggle-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-toggle-btn {
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
font-size: 1.1rem;
|
||||
color: var(--header-bg) !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
.sidebar-toggle-btn:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.sidebar-column {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.navigation-section {
|
||||
background-color: var(--nav-bg) !important;
|
||||
color: var(--nav-font) !important;
|
||||
min-height: var(--nav-height);
|
||||
}
|
||||
|
||||
.focus-visible:focus,
|
||||
.btn:focus,
|
||||
.form-control:focus,
|
||||
.nav-link:focus {
|
||||
outline: 3px solid #0056b3 !important;
|
||||
outline-offset: 2px !important;
|
||||
box-shadow: 0 0 0 1px #ffffff, 0 0 0 4px #0056b3 !important;
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
:root {
|
||||
--text-color: #000000;
|
||||
--bg-color: #ffffff;
|
||||
--border-color: #000000;
|
||||
--focus-color: #000000;
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: #000000 !important;
|
||||
border-color: #000000 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.btn-outline-light {
|
||||
color: #000000 !important;
|
||||
border-color: #000000 !important;
|
||||
}
|
||||
.text-muted {
|
||||
color: #000000 !important;
|
||||
}
|
||||
.navbar {
|
||||
background-color: #ffffff !important;
|
||||
border-bottom: 1px solid #000000 !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
.nav-tabs {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
.nav-tabs .nav-link {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
color: var(--nav-font) !important;
|
||||
}
|
||||
.nav-tabs .nav-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
.nav-tabs .nav-link.active {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
border-bottom: 2px solid var(--nav-font) !important;
|
||||
}
|
||||
|
||||
.sidebar-column {
|
||||
background-color: var(--sidebar-bg) !important;
|
||||
border-right: 1px solid var(--sidebar-border) !important;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
min-height: calc(100vh - var(--header-height) - var(--nav-height) - 42px);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding: 1.5rem;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-column {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 2rem;
|
||||
padding-bottom: 80px !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.sidebar-column {
|
||||
border-right: none !important;
|
||||
border-top: 1px solid var(--sidebar-border) !important;
|
||||
min-height: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.content-column {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.content-wrapper {
|
||||
min-height: auto;
|
||||
padding-bottom: 2rem !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
.sidebar-column {
|
||||
order: 2 !important;
|
||||
}
|
||||
.content-column {
|
||||
order: 1 !important;
|
||||
}
|
||||
}
|
||||
pre {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #dee2e6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: #333;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #e8e8e8;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
.footer-icon {
|
||||
color: #6c757d;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
}
|
||||
.footer-icon:hover {
|
||||
color: #0d6efd;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.footer-icon:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.footer-icon.guide:hover {
|
||||
color: #198754;
|
||||
}
|
||||
.footer-icon.cms:hover {
|
||||
color: #dc3545;
|
||||
}
|
||||
.footer-icon.git:hover {
|
||||
color: #6f42c1;
|
||||
}
|
||||
.footer-icon.website:hover {
|
||||
color: #fd7e14;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
1786279979
|
||||
@@ -1,323 +0,0 @@
|
||||
#site-header .search-input {
|
||||
background-color: #ffffff !important;
|
||||
color: #212529 !important;
|
||||
border: 2px solid #0a369d !important;
|
||||
}
|
||||
|
||||
#site-header .search-input::placeholder {
|
||||
color: #6c757d !important;
|
||||
}
|
||||
|
||||
#site-header .search-input:focus {
|
||||
background-color: #ffffff !important;
|
||||
color: #212529 !important;
|
||||
border-color: #2754b4 !important;
|
||||
box-shadow: 0 0 0 0.2rem rgba(10, 54, 157, 0.25) !important;
|
||||
}
|
||||
|
||||
:root {
|
||||
--header-bg: #0a369d;
|
||||
--header-font: #ffffff;
|
||||
--header-height: 56px;
|
||||
--nav-bg: #2754b4;
|
||||
--nav-font: #ffffff;
|
||||
--nav-height: 42px;
|
||||
--sidebar-bg: #f8f9fa;
|
||||
--sidebar-border: #dee2e6;
|
||||
}
|
||||
|
||||
#site-header {
|
||||
background-image: none;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: var(--header-bg) !important;
|
||||
min-height: var(--header-height);
|
||||
}
|
||||
.navbar .navbar-brand,
|
||||
.navbar .navbar-text,
|
||||
.navbar .btn {
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
.navbar .form-control {
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
.navbar .form-control::placeholder {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
.navbar .search-input {
|
||||
background-color: #ffffff !important;
|
||||
color: #212529 !important;
|
||||
border: 2px solid #0a369d !important;
|
||||
}
|
||||
.navbar .search-input::placeholder {
|
||||
color: #6c757d !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.navbar .search-input:focus {
|
||||
background-color: #ffffff !important;
|
||||
color: #212529 !important;
|
||||
border-color: #2754b4 !important;
|
||||
box-shadow: 0 0 0 0.2rem rgba(10, 54, 157, 0.25) !important;
|
||||
}
|
||||
.navbar .btn-outline-light {
|
||||
border-color: var(--header-font) !important;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background-color: var(--header-bg) !important;
|
||||
border: 1px solid var(--header-font) !important;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
.dropdown-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.btn-outline-light {
|
||||
color: var(--header-font) !important;
|
||||
border-color: var(--header-font) !important;
|
||||
}
|
||||
.btn-outline-light:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
.btn-outline-light:focus, .btn-outline-light:active, .show > .btn-outline-light.dropdown-toggle {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--header-font) !important;
|
||||
border-color: var(--header-font) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.bi-chevron-down {
|
||||
font-size: 0.75em;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
content: "" !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
--bs-breadcrumb-divider: "";
|
||||
--bs-breadcrumb-margin-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.breadcrumb .breadcrumb-item {
|
||||
color: var(--header-bg) !important;
|
||||
}
|
||||
.breadcrumb .breadcrumb-item a {
|
||||
color: var(--header-bg) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
.breadcrumb .breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.sidebar-toggle-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-toggle-btn {
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
font-size: 1.1rem;
|
||||
color: var(--header-bg) !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
.sidebar-toggle-btn:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.sidebar-column {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.navigation-section {
|
||||
background-color: var(--nav-bg) !important;
|
||||
color: var(--nav-font) !important;
|
||||
min-height: var(--nav-height);
|
||||
}
|
||||
|
||||
.focus-visible:focus,
|
||||
.btn:focus,
|
||||
.form-control:focus,
|
||||
.nav-link:focus {
|
||||
outline: 3px solid #0056b3 !important;
|
||||
outline-offset: 2px !important;
|
||||
box-shadow: 0 0 0 1px #ffffff, 0 0 0 4px #0056b3 !important;
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
:root {
|
||||
--text-color: #000000;
|
||||
--bg-color: #ffffff;
|
||||
--border-color: #000000;
|
||||
--focus-color: #000000;
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: #000000 !important;
|
||||
border-color: #000000 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.btn-outline-light {
|
||||
color: #000000 !important;
|
||||
border-color: #000000 !important;
|
||||
}
|
||||
.text-muted {
|
||||
color: #000000 !important;
|
||||
}
|
||||
.navbar {
|
||||
background-color: #ffffff !important;
|
||||
border-bottom: 1px solid #000000 !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
.nav-tabs {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
.nav-tabs .nav-link {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
color: var(--nav-font) !important;
|
||||
}
|
||||
.nav-tabs .nav-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
.nav-tabs .nav-link.active {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
border-bottom: 2px solid var(--nav-font) !important;
|
||||
}
|
||||
|
||||
.sidebar-column {
|
||||
background-color: var(--sidebar-bg) !important;
|
||||
border-right: 1px solid var(--sidebar-border) !important;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
min-height: calc(100vh - var(--header-height) - var(--nav-height) - 42px);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding: 1.5rem;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-column {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 2rem;
|
||||
padding-bottom: 80px !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.sidebar-column {
|
||||
border-right: none !important;
|
||||
border-top: 1px solid var(--sidebar-border) !important;
|
||||
min-height: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.content-column {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.content-wrapper {
|
||||
min-height: auto;
|
||||
padding-bottom: 2rem !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
.sidebar-column {
|
||||
order: 2 !important;
|
||||
}
|
||||
.content-column {
|
||||
order: 1 !important;
|
||||
}
|
||||
}
|
||||
pre {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #dee2e6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: #333;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #e8e8e8;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
.footer-icon {
|
||||
color: #6c757d;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
}
|
||||
.footer-icon:hover {
|
||||
color: #0d6efd;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.footer-icon:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.footer-icon.guide:hover {
|
||||
color: #198754;
|
||||
}
|
||||
.footer-icon.cms:hover {
|
||||
color: #dc3545;
|
||||
}
|
||||
.footer-icon.git:hover {
|
||||
color: #6f42c1;
|
||||
}
|
||||
.footer-icon.website:hover {
|
||||
color: #fd7e14;
|
||||
}
|
||||
|
After Width: | Height: | Size: 41 KiB |
@@ -93,34 +93,41 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// Handle nested dropdowns for touch devices using event delegation
|
||||
// Handle nested dropdowns — toggle submenu on click (for mobile/touch)
|
||||
// On desktop, CSS hover handles submenu display
|
||||
document.addEventListener('click', function(e) {
|
||||
const toggle = e.target.closest('.dropdown-submenu .dropdown-toggle');
|
||||
const toggle = e.target.closest('.dropdown-submenu > .dropdown-toggle');
|
||||
|
||||
if (toggle) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const submenu = toggle.closest('.dropdown-submenu');
|
||||
const submenu = toggle.parentElement;
|
||||
const dropdown = submenu.querySelector('.dropdown-menu');
|
||||
|
||||
// Close other submenus at the same level
|
||||
// Close other open submenus at the same level
|
||||
const parent = submenu.parentElement;
|
||||
parent.querySelectorAll('.dropdown-submenu').forEach(function(sibling) {
|
||||
parent.querySelectorAll(':scope > .dropdown-submenu').forEach(function(sibling) {
|
||||
if (sibling !== submenu) {
|
||||
sibling.classList.remove('show');
|
||||
var siblingMenu = sibling.querySelector('.dropdown-menu');
|
||||
if (siblingMenu) siblingMenu.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle current submenu
|
||||
if (dropdown) dropdown.classList.toggle('show');
|
||||
if (dropdown) {
|
||||
dropdown.classList.toggle('show');
|
||||
submenu.classList.toggle('show');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Close all open submenus when clicking outside
|
||||
document.querySelectorAll('.dropdown-submenu .dropdown-menu.show').forEach(function(menu) {
|
||||
menu.classList.remove('show');
|
||||
document.querySelectorAll('.dropdown-submenu.show').forEach(function(sm) {
|
||||
sm.classList.remove('show');
|
||||
var menu = sm.querySelector('.dropdown-menu');
|
||||
if (menu) menu.classList.remove('show');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,21 +106,43 @@ $header-bg-opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Language dropdown styling
|
||||
// Dropdown menu styling — override Bootstrap variables completely
|
||||
.dropdown-menu {
|
||||
--bs-dropdown-min-width: 0;
|
||||
--bs-dropdown-padding-x: 0;
|
||||
--bs-dropdown-padding-y: 0;
|
||||
--bs-dropdown-border-width: 0;
|
||||
--bs-dropdown-border-radius: 0;
|
||||
--bs-dropdown-box-shadow: none;
|
||||
background-color: var(--header-bg) !important;
|
||||
border: 1px solid var(--header-font) !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
min-width: 0 !important;
|
||||
box-shadow: none !important;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: var(--header-font) !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 0.4rem 1rem;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
&:hover, &:focus {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove Bootstrap dropdown border/spacing on nav-tabs dropdown
|
||||
.nav-tabs .dropdown-menu {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
// Hide Bootstrap dropdown arrow and use custom icon
|
||||
.dropdown-toggle::after {
|
||||
display: none !important;
|
||||
@@ -217,8 +239,7 @@ $header-bg-opacity: 1;
|
||||
// Enhanced accessibility styles
|
||||
.focus-visible:focus,
|
||||
.btn:focus,
|
||||
.form-control:focus,
|
||||
.nav-link:focus {
|
||||
.form-control:focus {
|
||||
outline: 3px solid #0056b3 !important;
|
||||
outline-offset: 2px !important;
|
||||
box-shadow: 0 0 0 1px #ffffff, 0 0 0 4px #0056b3 !important;
|
||||
@@ -266,25 +287,119 @@ $header-bg-opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove nav-tabs background so it inherits from parent
|
||||
// Navigation tabs — completely override Bootstrap nav-tabs styling
|
||||
.nav-tabs {
|
||||
--bs-nav-tabs-border-width: 0 !important;
|
||||
--bs-nav-tabs-border-color: transparent !important;
|
||||
--bs-nav-tabs-border-radius: 0 !important;
|
||||
--bs-nav-tabs-link-hover-border-color: transparent !important;
|
||||
--bs-nav-tabs-link-active-color: var(--nav-font) !important;
|
||||
--bs-nav-tabs-link-active-bg: transparent !important;
|
||||
--bs-nav-tabs-link-active-border-color: transparent !important;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
border-bottom: none !important;
|
||||
border-radius: 0 !important;
|
||||
gap: 0 !important;
|
||||
|
||||
.nav-link {
|
||||
margin-bottom: 0 !important;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
color: var(--nav-font) !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:hover {
|
||||
&:hover, &:focus, &:focus-visible {
|
||||
isolation: auto;
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
border-bottom: 2px solid var(--nav-font) !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
margin-top: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap 5 nested dropdown support (dropdown-submenu) — unlimited depth, CSS hover
|
||||
.dropdown-submenu {
|
||||
position: relative;
|
||||
|
||||
> .dropdown-menu {
|
||||
top: 0;
|
||||
left: 100%;
|
||||
margin-top: 0;
|
||||
margin-left: 0;
|
||||
display: none;
|
||||
--bs-dropdown-border-width: 0;
|
||||
--bs-dropdown-border-radius: 0;
|
||||
--bs-dropdown-box-shadow: none;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
// Open submenu on hover (desktop)
|
||||
&:hover > .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Keep parent highlighted when hovering submenu
|
||||
&:hover > .dropdown-toggle {
|
||||
background-color: rgba(255, 255, 255, 0.15) !important;
|
||||
}
|
||||
|
||||
// Arrow pointing right — vertically centered with text
|
||||
> .dropdown-toggle {
|
||||
display: flex !important;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
.bi-chevron-right {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile: submenu appears below instead of beside, open on tap
|
||||
@media (max-width: 767.98px) {
|
||||
.dropdown-submenu > .dropdown-menu {
|
||||
left: 0;
|
||||
top: 100%;
|
||||
margin-top: 0;
|
||||
margin-left: 1rem;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
.dropdown-submenu:hover > .dropdown-menu {
|
||||
display: none;
|
||||
}
|
||||
.dropdown-submenu.show > .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar styling
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<link rel="me" href="{{ author_git }}">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="{{ theme_base_url }}/assets/img/favicon.ico" sizes="any">
|
||||
<link rel="icon" type="image/x-icon" href="{{ theme_base_url }}/assets/img/favicon.ico">
|
||||
<link rel="icon" type="image/svg+xml" href="{{ theme_base_url }}/assets/img/favicon.svg">
|
||||
<link rel="apple-touch-icon" href="{{ theme_base_url }}/assets/img/favicon.svg">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/* Mobile search improvements */
|
||||
@media (max-width: 991.98px) {
|
||||
.navbar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Hide desktop elements on mobile */
|
||||
.d-none.d-lg-flex {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#mobileSearch {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--header-bg);
|
||||
z-index: 1030;
|
||||
}
|
||||
|
||||
#mobileSearch.show {
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Ensure navigation doesn't overlap with expanded search */
|
||||
.navigation-section {
|
||||
margin-top: 0;
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop improvements */
|
||||
@media (min-width: 992px) {
|
||||
/* Hide mobile elements on desktop */
|
||||
.d-lg-none {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Hide mobile search on desktop */
|
||||
#mobileSearch {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Search form improvements */
|
||||
.form-control[type="search"] {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.form-control[type="search"] {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/*!
|
||||
* Bootstrap v5.3.8 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
@@ -1 +0,0 @@
|
||||
1786274744
|
||||
@@ -1,266 +0,0 @@
|
||||
:root {
|
||||
--header-bg: #613583;
|
||||
--header-font: #ffffff;
|
||||
--header-height: 120px;
|
||||
--nav-bg: #813d9c;
|
||||
--nav-font: #ffffff;
|
||||
--nav-height: 50px;
|
||||
--sidebar-bg: #f3e8f7;
|
||||
--sidebar-border: #d8b4e0;
|
||||
}
|
||||
|
||||
#site-header {
|
||||
background-image: none;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: var(--header-bg) !important;
|
||||
min-height: var(--header-height);
|
||||
border-bottom: 4px solid var(--nav-bg) !important;
|
||||
}
|
||||
.navbar .navbar-brand,
|
||||
.navbar .navbar-text,
|
||||
.navbar .form-control,
|
||||
.navbar .btn {
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
.navbar .form-control::placeholder {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
.navbar .btn-outline-light {
|
||||
border-color: var(--header-font) !important;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background-color: var(--header-bg) !important;
|
||||
border: 1px solid var(--header-font) !important;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
.dropdown-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.btn-outline-light {
|
||||
color: var(--header-font) !important;
|
||||
border-color: var(--header-font) !important;
|
||||
}
|
||||
.btn-outline-light:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
.btn-outline-light:focus, .btn-outline-light:active, .show > .btn-outline-light.dropdown-toggle {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--header-font) !important;
|
||||
border-color: var(--header-font) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.bi-chevron-down {
|
||||
font-size: 0.75em;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
content: "" !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
--bs-breadcrumb-divider: "";
|
||||
--bs-breadcrumb-margin-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.breadcrumb .breadcrumb-item {
|
||||
color: var(--header-bg) !important;
|
||||
}
|
||||
.breadcrumb .breadcrumb-item a {
|
||||
color: var(--header-bg) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
.breadcrumb .breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.sidebar-toggle-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-toggle-btn {
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
font-size: 1.1rem;
|
||||
color: var(--header-bg) !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
.sidebar-toggle-btn:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.sidebar-column {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.navigation-section {
|
||||
background-color: var(--nav-bg) !important;
|
||||
color: var(--nav-font) !important;
|
||||
min-height: var(--nav-height);
|
||||
}
|
||||
|
||||
.focus-visible:focus,
|
||||
.btn:focus,
|
||||
.form-control:focus,
|
||||
.nav-link:focus {
|
||||
outline: 3px solid #0056b3 !important;
|
||||
outline-offset: 2px !important;
|
||||
box-shadow: 0 0 0 1px #ffffff, 0 0 0 4px #0056b3 !important;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
.nav-tabs {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
.nav-tabs .nav-link {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
color: var(--nav-font) !important;
|
||||
}
|
||||
.nav-tabs .nav-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
.nav-tabs .nav-link.active {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
border-bottom: 2px solid var(--nav-font) !important;
|
||||
}
|
||||
|
||||
.sidebar-column {
|
||||
background-color: var(--sidebar-bg) !important;
|
||||
border-right: 1px solid var(--sidebar-border) !important;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
min-height: calc(100vh - var(--header-height) - var(--nav-height) - 42px);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding: 1.5rem;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.sidebar .card {
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--sidebar-border);
|
||||
}
|
||||
|
||||
.content-column {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 2rem;
|
||||
padding-bottom: 80px !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.sidebar-column {
|
||||
border-right: none !important;
|
||||
border-top: 1px solid var(--sidebar-border) !important;
|
||||
min-height: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.content-wrapper {
|
||||
min-height: auto;
|
||||
padding-bottom: 2rem !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
.sidebar-column {
|
||||
order: 2 !important;
|
||||
}
|
||||
.content-column {
|
||||
order: 1 !important;
|
||||
}
|
||||
}
|
||||
pre {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #dee2e6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: #333;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #e8e8e8;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
.footer-icon {
|
||||
color: #6c757d;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
}
|
||||
.footer-icon:hover {
|
||||
color: #0d6efd;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.footer-icon:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.footer-icon.guide:hover {
|
||||
color: #198754;
|
||||
}
|
||||
.footer-icon.cms:hover {
|
||||
color: #dc3545;
|
||||
}
|
||||
.footer-icon.git:hover {
|
||||
color: #6f42c1;
|
||||
}
|
||||
.footer-icon.website:hover {
|
||||
color: #fd7e14;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 31 KiB |
@@ -1,11 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- < -->
|
||||
<path d="M8 8 L3 16 L8 24" stroke="#ffffff" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
<!-- / -->
|
||||
<path d="M12 24 L18 8" stroke="#ffffff" stroke-width="3" stroke-linecap="round"/>
|
||||
|
||||
<!-- .. -->
|
||||
<circle cx="22" cy="20" r="2" fill="#ffffff"/>
|
||||
<circle cx="28" cy="20" r="2" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 442 B |
@@ -1,11 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- < -->
|
||||
<path d="M8 8 L3 16 L8 24" stroke="#ffffff" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
<!-- / -->
|
||||
<path d="M12 24 L18 8" stroke="#ffffff" stroke-width="3" stroke-linecap="round"/>
|
||||
|
||||
<!-- .. -->
|
||||
<circle cx="22" cy="20" r="2" fill="#ffffff"/>
|
||||
<circle cx="28" cy="20" r="2" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 442 B |
@@ -1,94 +0,0 @@
|
||||
// Main application JavaScript
|
||||
// This file contains general application functionality
|
||||
|
||||
/**
|
||||
* Toggle sidebar visibility (open/close)
|
||||
*/
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('site-sidebar');
|
||||
const contentCol = sidebar ? sidebar.nextElementSibling || sidebar.parentElement.querySelector('.content-column') : null;
|
||||
const btn = document.querySelector('.sidebar-toggle-btn');
|
||||
const icon = btn ? btn.querySelector('i') : null;
|
||||
|
||||
if (!sidebar) return;
|
||||
|
||||
sidebar.classList.toggle('sidebar-hidden');
|
||||
|
||||
// Adjust content column width, toggle icon, and update aria-expanded
|
||||
if (sidebar.classList.contains('sidebar-hidden')) {
|
||||
if (contentCol) {
|
||||
contentCol.classList.remove('col-lg-9', 'col-md-8');
|
||||
contentCol.classList.add('col-12');
|
||||
}
|
||||
if (icon) {
|
||||
icon.classList.remove('bi-layout-sidebar-inset');
|
||||
icon.classList.add('bi-layout-sidebar');
|
||||
}
|
||||
if (btn) {
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
sessionStorage.setItem('sidebarHidden', 'true');
|
||||
} else {
|
||||
if (contentCol) {
|
||||
contentCol.classList.remove('col-12');
|
||||
contentCol.classList.add('col-lg-9', 'col-md-8');
|
||||
}
|
||||
if (icon) {
|
||||
icon.classList.remove('bi-layout-sidebar');
|
||||
icon.classList.add('bi-layout-sidebar-inset');
|
||||
}
|
||||
if (btn) {
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
sessionStorage.setItem('sidebarHidden', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore sidebar state from sessionStorage on page load
|
||||
*/
|
||||
function restoreSidebarState() {
|
||||
if (sessionStorage.getItem('sidebarHidden') === 'true') {
|
||||
const sidebar = document.getElementById('site-sidebar');
|
||||
if (sidebar) {
|
||||
toggleSidebar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize application when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Restore sidebar state
|
||||
restoreSidebarState();
|
||||
|
||||
// Handle nested dropdowns for touch devices using event delegation
|
||||
document.addEventListener('click', function(e) {
|
||||
const toggle = e.target.closest('.dropdown-submenu .dropdown-toggle');
|
||||
|
||||
if (toggle) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const submenu = toggle.closest('.dropdown-submenu');
|
||||
const dropdown = submenu.querySelector('.dropdown-menu');
|
||||
|
||||
// Close other submenus at the same level
|
||||
const parent = submenu.parentElement;
|
||||
parent.querySelectorAll('.dropdown-submenu').forEach(function(sibling) {
|
||||
if (sibling !== submenu) {
|
||||
var siblingMenu = sibling.querySelector('.dropdown-menu');
|
||||
if (siblingMenu) siblingMenu.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle current submenu
|
||||
if (dropdown) dropdown.classList.toggle('show');
|
||||
return;
|
||||
}
|
||||
|
||||
// Close all open submenus when clicking outside
|
||||
document.querySelectorAll('.dropdown-submenu .dropdown-menu.show').forEach(function(menu) {
|
||||
menu.classList.remove('show');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,333 +0,0 @@
|
||||
// CodePress demo theme styles
|
||||
// A distinct look to demonstrate that themes are fully swappable.
|
||||
|
||||
// Theme color variables (defined per theme; used by the CSS custom properties below)
|
||||
$header-bg: #613583;
|
||||
$header-font: #ffffff;
|
||||
$header-height: 120px;
|
||||
$nav-bg: #813d9c;
|
||||
$nav-font: #ffffff;
|
||||
$nav-height: 50px;
|
||||
$sidebar-bg: #f3e8f7;
|
||||
$sidebar-border: #d8b4e0;
|
||||
|
||||
// Optional header background image and overlay opacity
|
||||
$header-bg-image: none;
|
||||
$header-bg-opacity: 1;
|
||||
|
||||
:root {
|
||||
--header-bg: #{$header-bg};
|
||||
--header-font: #{$header-font};
|
||||
--header-height: #{$header-height};
|
||||
--nav-bg: #{$nav-bg};
|
||||
--nav-font: #{$nav-font};
|
||||
--nav-height: #{$nav-height};
|
||||
--sidebar-bg: #{$sidebar-bg};
|
||||
--sidebar-border: #{$sidebar-border};
|
||||
}
|
||||
|
||||
// Header background (optional background image via $header-bg-image)
|
||||
#site-header {
|
||||
background-image: $header-bg-image;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@if $header-bg-image != 'none' {
|
||||
#site-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--header-bg);
|
||||
opacity: $header-bg-opacity;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Rounded, softer header
|
||||
.navbar {
|
||||
background-color: var(--header-bg) !important;
|
||||
min-height: var(--header-height);
|
||||
border-bottom: 4px solid var(--nav-bg) !important;
|
||||
|
||||
.navbar-brand,
|
||||
.navbar-text,
|
||||
.form-control,
|
||||
.btn {
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
|
||||
.form-control::placeholder {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
|
||||
.btn-outline-light {
|
||||
border-color: var(--header-font) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Language dropdown styling
|
||||
.dropdown-menu {
|
||||
background-color: var(--header-bg) !important;
|
||||
border: 1px solid var(--header-font) !important;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
color: var(--header-font) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.btn-outline-light {
|
||||
color: var(--header-font) !important;
|
||||
border-color: var(--header-font) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--header-font) !important;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
.show > &.dropdown-toggle {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
color: var(--header-font) !important;
|
||||
border-color: var(--header-font) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bi-chevron-down {
|
||||
font-size: 0.75em;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
// Breadcrumb
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
content: "" !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
--bs-breadcrumb-divider: "";
|
||||
--bs-breadcrumb-margin-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
.breadcrumb-item {
|
||||
color: var(--header-bg) !important;
|
||||
|
||||
a {
|
||||
color: var(--header-bg) !important;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-toggle-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar-toggle-btn {
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
font-size: 1.1rem;
|
||||
color: var(--header-bg) !important;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-column {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// Navigation section background
|
||||
.navigation-section {
|
||||
background-color: var(--nav-bg) !important;
|
||||
color: var(--nav-font) !important;
|
||||
min-height: var(--nav-height);
|
||||
}
|
||||
|
||||
// Accessibility
|
||||
.focus-visible:focus,
|
||||
.btn:focus,
|
||||
.form-control:focus,
|
||||
.nav-link:focus {
|
||||
outline: 3px solid #0056b3 !important;
|
||||
outline-offset: 2px !important;
|
||||
box-shadow: 0 0 0 1px #ffffff, 0 0 0 4px #0056b3 !important;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
|
||||
.nav-link {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
color: var(--nav-font) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
border-bottom: 2px solid var(--nav-font) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar styling - rounded cards look
|
||||
.sidebar-column {
|
||||
background-color: var(--sidebar-bg) !important;
|
||||
border-right: 1px solid var(--sidebar-border) !important;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
min-height: calc(100vh - var(--header-height) - var(--nav-height) - 42px);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding: 1.5rem;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
.card {
|
||||
border-radius: 1rem;
|
||||
border: 1px solid var(--sidebar-border);
|
||||
}
|
||||
}
|
||||
|
||||
.content-column {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 2rem;
|
||||
padding-bottom: 80px !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// Mobile responsive
|
||||
@media (max-width: 767.98px) {
|
||||
.sidebar-column {
|
||||
border-right: none !important;
|
||||
border-top: 1px solid var(--sidebar-border) !important;
|
||||
min-height: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
min-height: auto;
|
||||
padding-bottom: 2rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.sidebar-column {
|
||||
order: 2 !important;
|
||||
}
|
||||
|
||||
.content-column {
|
||||
order: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Code block styling
|
||||
pre {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #dee2e6;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: #333;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
background: #e8e8e8;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
// Footer icon hover effects
|
||||
.footer-icon {
|
||||
color: #6c757d;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
|
||||
&:hover {
|
||||
color: #0d6efd;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&.guide:hover {
|
||||
color: #198754;
|
||||
}
|
||||
|
||||
&.cms:hover {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
&.git:hover {
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
&.website:hover {
|
||||
color: #fd7e14;
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ current_lang }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ page_title }} - {{ site_title }}</title>
|
||||
|
||||
<!-- Skip to content link for accessibility -->
|
||||
<a href="#main-content" class="skip-link sr-only sr-only-focusable">Skip to main content</a>
|
||||
|
||||
<!-- CMS Meta Tags -->
|
||||
<meta name="generator" content="{{ site_title }} CMS">
|
||||
<meta name="application-name" content="{{ site_title }}">
|
||||
<meta name="author" content="{{ author_name }}">
|
||||
<meta name="creator" content="{{ author_name }}">
|
||||
<meta name="publisher" content="{{ author_name }}">
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
<meta name="description" content="{{ seo_description }}">
|
||||
<meta name="keywords" content="{{ seo_keywords }}">
|
||||
{% if block_ai_bots %}
|
||||
<meta name="robots" content="noai, noimageai">
|
||||
<meta name="tdm-reservation" content="1">
|
||||
{% endif %}
|
||||
{% if block_search_engines %}
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
{% endif %}
|
||||
|
||||
<!-- Author Links -->
|
||||
<link rel="author" href="{{ author_website }}">
|
||||
<link rel="me" href="{{ author_git }}">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" href="{{ theme_base_url }}/assets/img/favicon.ico" sizes="any">
|
||||
<link rel="icon" type="image/svg+xml" href="{{ theme_base_url }}/assets/img/favicon.svg">
|
||||
<link rel="apple-touch-icon" href="{{ theme_base_url }}/assets/img/favicon.svg">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<meta name="theme-color" content="{{ theme_config.header_color|default('#613583') }}">
|
||||
|
||||
<!-- Theme CSS -->
|
||||
{% for cssFile in theme_css_files %}
|
||||
<link href="{{ cssFile }}" rel="stylesheet">
|
||||
{% endfor %}
|
||||
|
||||
<!-- Compiled theme CSS (if SCSS was used) -->
|
||||
{% if theme_css_url %}
|
||||
<link href="{{ theme_css_url }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
|
||||
<!-- Accessibility styles -->
|
||||
<style>
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 6px;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
padding: 8px;
|
||||
text-decoration: none;
|
||||
z-index: 100;
|
||||
}
|
||||
.skip-link:focus {
|
||||
top: 6px;
|
||||
outline: 3px solid #0056b3;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
.sr-only-focusable:focus {
|
||||
position: static;
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: inherit;
|
||||
margin: inherit;
|
||||
overflow: visible;
|
||||
clip: auto;
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Dynamic theme styles -->
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#site-header > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% include 'partials/header.twig' %}
|
||||
|
||||
<nav role="navigation" aria-label="Main navigation" id="site-navigation">
|
||||
{% include 'partials/navigation.twig' %}
|
||||
</nav>
|
||||
|
||||
<nav id="site-breadcrumb" class="breadcrumb-section bg-light border-bottom" aria-label="Breadcrumb navigation">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 py-2">
|
||||
<h2 class="sr-only">Breadcrumb Navigation</h2>
|
||||
{{ breadcrumb|raw }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main role="main" id="main-content" class="main-content" style="padding: 0;">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer role="contentinfo" id="site-footer">
|
||||
{% include 'partials/footer.twig' %}
|
||||
</footer>
|
||||
|
||||
<!-- Theme JS -->
|
||||
{% for jsFile in theme_js_files %}
|
||||
<script src="{{ jsFile }}"></script>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Theme-specific JS -->
|
||||
{% if theme_js_url %}
|
||||
<script src="{{ theme_js_url }}"></script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +0,0 @@
|
||||
{% extends 'base.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row g-0">
|
||||
<aside role="complementary" aria-label="Sidebar content" id="site-sidebar" class="col-12 sidebar-column">
|
||||
<div class="sidebar">
|
||||
{{ sidebar_content|raw }}
|
||||
</div>
|
||||
</aside>
|
||||
<section id="site-content" class="col-12">
|
||||
<div class="content-wrapper p-4">
|
||||
{{ content|raw }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,11 +0,0 @@
|
||||
{% extends 'base.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<section id="site-content" class="col-12">
|
||||
<div class="content-wrapper p-4">
|
||||
{{ content|raw }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,32 +0,0 @@
|
||||
{% extends 'base.twig' %}
|
||||
|
||||
{% block title %}{{ page.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if sidebar_content %}
|
||||
<div class="row g-0">
|
||||
<aside role="complementary" aria-label="Handleiding navigatie" id="site-sidebar" class="col-lg-3 col-md-4 sidebar-column order-2 order-md-1">
|
||||
<div class="sidebar h-100">
|
||||
{{ sidebar_content|raw }}
|
||||
</div>
|
||||
</aside>
|
||||
<section id="site-content" class="col-lg-9 col-md-8 content-column order-1 order-md-2">
|
||||
<div class="content-wrapper p-4">
|
||||
<div class="guide-content">
|
||||
{{ content|raw }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="container">
|
||||
<section id="site-content" class="col-12">
|
||||
<div class="content-wrapper p-4">
|
||||
<div class="guide-content">
|
||||
{{ content|raw }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,26 +0,0 @@
|
||||
{% extends 'base.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% if sidebar_content %}
|
||||
<div class="row g-0">
|
||||
<aside role="complementary" aria-label="Sidebar content" id="site-sidebar" class="col-lg-3 col-md-4 sidebar-column order-2 order-md-1">
|
||||
<div class="sidebar h-100">
|
||||
{{ sidebar_content|raw }}
|
||||
</div>
|
||||
</aside>
|
||||
<section id="site-content" class="col-lg-9 col-md-8 content-column order-1 order-md-2">
|
||||
<div class="content-wrapper p-4">
|
||||
{{ content|raw }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="container">
|
||||
<section id="site-content" class="col-12">
|
||||
<div class="content-wrapper p-4">
|
||||
{{ content|raw }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,48 +0,0 @@
|
||||
<footer class="bg-light border-top py-1">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center">
|
||||
<div class="file-info mb-2 mb-md-0">
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-file-text footer-icon" title="{{ t_file_details }}: {{ page_title }}"></i>
|
||||
<span class="page-title d-none d-lg-inline" title="{{ page_title }}">{{ page_title }}</span>
|
||||
{% if file_info_block %}
|
||||
<span class="ms-2">
|
||||
{% if show_created %}
|
||||
<i class="bi bi-calendar-plus footer-icon" title="{{ t_created }}: {{ created }}"></i>
|
||||
<span class="file-created me-1" title="{{ t_created }}: {{ created }}">{{ created }}</span>
|
||||
{% endif %}
|
||||
<i class="bi bi-calendar-check footer-icon" title="{{ t_modified }}: {{ modified }}"></i>
|
||||
<span class="file-modified" title="{{ t_modified }}: {{ modified }}">{{ modified }}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
<div class="site-info">
|
||||
<small class="text-muted">
|
||||
<a href="/{{ current_lang }}/guide" class="footer-icon guide" title="{{ t_guide }}">
|
||||
<i class="bi bi-book"></i>
|
||||
</a>
|
||||
<span class="ms-1">|</span>
|
||||
{% if cms_version %}
|
||||
<span class="ms-1 cms-version text-muted">{{ cms_version }}</span>
|
||||
{% endif %}
|
||||
<a href="https://git.noorlander.info/E.Noorlander/CodePress.git" target="_blank" rel="noopener noreferrer" class="footer-icon cms ms-1" title="{{ t_powered_by }} CodePress CMS">
|
||||
<i class="bi bi-cpu"></i>
|
||||
</a>
|
||||
<span class="ms-1">|</span>
|
||||
<a href="{{ author_website }}" target="_blank" rel="noopener noreferrer" class="footer-icon website" title="{{ t_author_website }}">
|
||||
<i class="bi bi-globe"></i>
|
||||
</a>
|
||||
<span class="ms-1">|</span>
|
||||
<a href="{{ author_git }}" target="_blank" rel="noopener noreferrer" class="footer-icon git" title="{{ t_author_git }}">
|
||||
<i class="bi bi-git"></i>
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -1,75 +0,0 @@
|
||||
<header id="site-header" class="navbar navbar-expand-lg navbar-dark" style="background-color: transparent;">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/{{ current_lang }}">
|
||||
<img src="{{ theme_base_url }}/assets/img/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
|
||||
{{ site_title }}
|
||||
</a>
|
||||
|
||||
<!-- Desktop search and language -->
|
||||
<div class="d-none d-lg-flex ms-auto align-items-center">
|
||||
<form class="d-flex me-3" method="GET" action="" role="search" aria-label="Site search">
|
||||
<div class="form-group">
|
||||
<label for="desktop-search-input" class="sr-only">{{ t_search_placeholder }}</label>
|
||||
<input class="form-control me-2 search-input" type="search" id="desktop-search-input" name="search" placeholder="{{ t_search_placeholder }}" value="{{ search_query }}" aria-describedby="search-help">
|
||||
<div id="search-help" class="sr-only">Enter keywords to search through the documentation</div>
|
||||
</div>
|
||||
<button class="btn btn-outline-light" type="submit" aria-label="{{ t_search_button }}">
|
||||
<i class="bi bi-search" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ t_search_button }}</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Language switcher -->
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-light" type="button" data-bs-toggle="dropdown" aria-haspopup="menu" aria-expanded="false" aria-label="Select language - currently {{ current_lang_upper }}">
|
||||
{{ current_lang_upper }} <i class="bi bi-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" role="menu">
|
||||
{% for lang in available_langs %}
|
||||
<li role="none">
|
||||
<a class="dropdown-item {{ lang.is_current ? 'active' : '' }}" href="{{ lang.url }}" role="menuitem" {% if lang.is_current %}aria-current="true"{% endif %} lang="{{ lang.code }}">
|
||||
{{ lang.native_name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile search and language toggle -->
|
||||
<div class="d-lg-none">
|
||||
<button class="btn btn-outline-light" type="button" data-bs-toggle="collapse" data-bs-target="#mobileSearch" aria-controls="mobileSearch" aria-expanded="false" aria-label="Toggle search">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-light" type="button" data-bs-toggle="dropdown" aria-haspopup="menu" aria-expanded="false" aria-label="Select language - currently {{ current_lang_upper }}">
|
||||
{{ current_lang_upper }} <i class="bi bi-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" role="menu">
|
||||
{% for lang in available_langs %}
|
||||
<li role="none">
|
||||
<a class="dropdown-item {{ lang.is_current ? 'active' : '' }}" href="{{ lang.url }}" role="menuitem" {% if lang.is_current %}aria-current="true"{% endif %} lang="{{ lang.code }}">
|
||||
{{ lang.native_name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile search bar -->
|
||||
<div class="collapse navbar-collapse d-lg-none" id="mobileSearch">
|
||||
<div class="container-fluid px-0">
|
||||
<form class="d-flex px-3 pb-3" method="GET" action="" role="search" aria-label="Site search">
|
||||
<div class="form-group w-100">
|
||||
<label for="mobile-search-input" class="sr-only">{{ t_search_placeholder }}</label>
|
||||
<input class="form-control me-2 search-input" type="search" id="mobile-search-input" name="search" placeholder="{{ t_search_placeholder }}" value="{{ search_query }}" aria-describedby="mobile-search-help">
|
||||
<div id="mobile-search-help" class="sr-only">Enter keywords to search through the documentation</div>
|
||||
</div>
|
||||
<button class="btn btn-outline-light" type="submit" aria-label="{{ t_search_button }}">
|
||||
<i class="bi bi-search" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{ t_search_button }}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1,17 +0,0 @@
|
||||
<nav class="navigation-section" role="navigation" aria-label="Main navigation">
|
||||
<h2 class="sr-only">Site Navigation</h2>
|
||||
<div class="container-fluid">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<ul class="nav nav-tabs flex-wrap" role="menubar">
|
||||
<li class="nav-item" role="none">
|
||||
<a class="nav-link {{ home_active_class }}" href="/{{ current_lang }}" role="menuitem" {% if is_homepage %}aria-current="page"{% endif %}>
|
||||
<i class="bi bi-house" aria-hidden="true"></i> {{ homepage_title }}
|
||||
</a>
|
||||
</li>
|
||||
{{ menu|raw }}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,26 +0,0 @@
|
||||
{% extends 'base.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% if sidebar_content %}
|
||||
<div class="row g-0">
|
||||
<section id="site-content" class="col-lg-9 col-md-8 content-column order-1">
|
||||
<div class="content-wrapper p-4">
|
||||
{{ content|raw }}
|
||||
</div>
|
||||
</section>
|
||||
<aside role="complementary" aria-label="Sidebar content" id="site-sidebar" class="col-lg-3 col-md-4 sidebar-column order-2">
|
||||
<div class="sidebar h-100">
|
||||
{{ sidebar_content|raw }}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="container">
|
||||
<section id="site-content" class="col-12">
|
||||
<div class="content-wrapper p-4">
|
||||
{{ content|raw }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"title": "demo",
|
||||
"config": {
|
||||
"default_template": "left_sidebar"
|
||||
},
|
||||
"template": {
|
||||
"full_content": "full_content.twig",
|
||||
"left_sidebar": "left_sidebar.twig",
|
||||
"right_sidebar": "right_sidebar.twig",
|
||||
"custom1": "custom1.twig",
|
||||
"guide": "guide.twig"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 31 KiB |
@@ -40,8 +40,7 @@ gen_preview() {
|
||||
echo "Generated $dir/theme.png"
|
||||
}
|
||||
|
||||
for d in default demo test; do
|
||||
if [[ -d "$d" ]]; then
|
||||
gen_preview "$d"
|
||||
fi
|
||||
for d in */; do
|
||||
d="${d%/}"
|
||||
gen_preview "$d"
|
||||
done
|
||||
|
||||
@@ -8,39 +8,6 @@ $baseDir = dirname($vendorDir);
|
||||
return array(
|
||||
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
'Mustache_Cache' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Cache_AbstractCache' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Cache_FilesystemCache' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Cache_NoopCache' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Compiler' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Context' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Engine' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_InvalidArgumentException' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_LogicException' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_RuntimeException' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_SyntaxException' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_UnknownFilterException' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_UnknownHelperException' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_UnknownTemplateException' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_HelperCollection' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_LambdaHelper' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_ArrayLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_CascadingLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_FilesystemLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_InlineLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_MutableLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_ProductionFilesystemLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_StringLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Logger' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Logger_AbstractLogger' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Logger_StreamLogger' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Parser' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Source' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Source_FilesystemSource' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Template' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Tokenizer' => $vendorDir . '/mustache/mustache/src/compat.php',
|
||||
'Nette\\ArgumentOutOfRangeException' => $vendorDir . '/nette/utils/src/exceptions.php',
|
||||
'Nette\\DeprecatedException' => $vendorDir . '/nette/utils/src/exceptions.php',
|
||||
'Nette\\DirectoryNotFoundException' => $vendorDir . '/nette/utils/src/exceptions.php',
|
||||
|
||||
@@ -18,11 +18,11 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
|
||||
);
|
||||
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'T' =>
|
||||
'T' =>
|
||||
array (
|
||||
'Twig\\' => 5,
|
||||
),
|
||||
'S' =>
|
||||
'S' =>
|
||||
array (
|
||||
'Symfony\\Polyfill\\Php80\\' => 23,
|
||||
'Symfony\\Polyfill\\Mbstring\\' => 26,
|
||||
@@ -31,18 +31,17 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
|
||||
'SourceSpan\\' => 11,
|
||||
'ScssPhp\\ScssPhp\\' => 16,
|
||||
),
|
||||
'P' =>
|
||||
'P' =>
|
||||
array (
|
||||
'Psr\\Log\\' => 8,
|
||||
'Psr\\Http\\Message\\' => 17,
|
||||
'Psr\\EventDispatcher\\' => 20,
|
||||
'PhpMqtt\\Client\\' => 15,
|
||||
),
|
||||
'N' =>
|
||||
'N' =>
|
||||
array (
|
||||
'Nette\\' => 6,
|
||||
),
|
||||
'M' =>
|
||||
'M' =>
|
||||
array (
|
||||
'MyCLabs\\Enum\\' => 13,
|
||||
'Mustache\\' => 9,
|
||||
@@ -50,119 +49,111 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
|
||||
'MaxMind\\Exception\\' => 18,
|
||||
'MaxMind\\Db\\' => 11,
|
||||
),
|
||||
'L' =>
|
||||
'L' =>
|
||||
array (
|
||||
'League\\Uri\\' => 11,
|
||||
'League\\Config\\' => 14,
|
||||
'League\\CommonMark\\' => 18,
|
||||
),
|
||||
'G' =>
|
||||
'G' =>
|
||||
array (
|
||||
'GeoIp2\\' => 7,
|
||||
),
|
||||
'D' =>
|
||||
'D' =>
|
||||
array (
|
||||
'Dflydev\\DotAccessData\\' => 22,
|
||||
),
|
||||
'C' =>
|
||||
'C' =>
|
||||
array (
|
||||
'Composer\\CaBundle\\' => 18,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'Twig\\' =>
|
||||
'Twig\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/twig/twig/src',
|
||||
),
|
||||
'Symfony\\Polyfill\\Php80\\' =>
|
||||
'Symfony\\Polyfill\\Php80\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
|
||||
),
|
||||
'Symfony\\Polyfill\\Mbstring\\' =>
|
||||
'Symfony\\Polyfill\\Mbstring\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
|
||||
),
|
||||
'Symfony\\Polyfill\\Ctype\\' =>
|
||||
'Symfony\\Polyfill\\Ctype\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
|
||||
),
|
||||
'Symfony\\Component\\Filesystem\\' =>
|
||||
'Symfony\\Component\\Filesystem\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/filesystem',
|
||||
),
|
||||
'SourceSpan\\' =>
|
||||
'SourceSpan\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/scssphp/source-span/src',
|
||||
),
|
||||
'ScssPhp\\ScssPhp\\' =>
|
||||
'ScssPhp\\ScssPhp\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/scssphp/scssphp/src',
|
||||
),
|
||||
'Psr\\Log\\' =>
|
||||
'Psr\\Log\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/log/src',
|
||||
),
|
||||
'Psr\\Http\\Message\\' =>
|
||||
'Psr\\Http\\Message\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/http-factory/src',
|
||||
1 => __DIR__ . '/..' . '/psr/http-message/src',
|
||||
),
|
||||
'Psr\\EventDispatcher\\' =>
|
||||
'Psr\\EventDispatcher\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/event-dispatcher/src',
|
||||
),
|
||||
'PhpMqtt\\Client\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/php-mqtt/client/src',
|
||||
),
|
||||
'Nette\\' =>
|
||||
'Nette\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/nette/schema/src',
|
||||
1 => __DIR__ . '/..' . '/nette/utils/src',
|
||||
),
|
||||
'MyCLabs\\Enum\\' =>
|
||||
'MyCLabs\\Enum\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/myclabs/php-enum/src',
|
||||
),
|
||||
'Mustache\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/mustache/mustache/src',
|
||||
),
|
||||
'MaxMind\\WebService\\' =>
|
||||
'MaxMind\\WebService\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/maxmind/web-service-common/src/WebService',
|
||||
),
|
||||
'MaxMind\\Exception\\' =>
|
||||
'MaxMind\\Exception\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/maxmind/web-service-common/src/Exception',
|
||||
),
|
||||
'MaxMind\\Db\\' =>
|
||||
'MaxMind\\Db\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db',
|
||||
),
|
||||
'League\\Uri\\' =>
|
||||
'League\\Uri\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/league/uri',
|
||||
1 => __DIR__ . '/..' . '/league/uri-interfaces',
|
||||
),
|
||||
'League\\Config\\' =>
|
||||
'League\\Config\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/league/config/src',
|
||||
),
|
||||
'League\\CommonMark\\' =>
|
||||
'League\\CommonMark\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/league/commonmark/src',
|
||||
),
|
||||
'GeoIp2\\' =>
|
||||
'GeoIp2\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/geoip2/geoip2/src',
|
||||
),
|
||||
'Dflydev\\DotAccessData\\' =>
|
||||
'Dflydev\\DotAccessData\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/dflydev/dot-access-data/src',
|
||||
),
|
||||
'Composer\\CaBundle\\' =>
|
||||
'Composer\\CaBundle\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/composer/ca-bundle/src',
|
||||
),
|
||||
@@ -171,39 +162,6 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
|
||||
public static $classMap = array (
|
||||
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
'Mustache_Cache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Cache_AbstractCache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Cache_FilesystemCache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Cache_NoopCache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Compiler' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Context' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Engine' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_InvalidArgumentException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_LogicException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_RuntimeException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_SyntaxException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_UnknownFilterException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_UnknownHelperException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Exception_UnknownTemplateException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_HelperCollection' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_LambdaHelper' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_ArrayLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_CascadingLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_FilesystemLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_InlineLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_MutableLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_ProductionFilesystemLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Loader_StringLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Logger' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Logger_AbstractLogger' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Logger_StreamLogger' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Parser' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Source' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Source_FilesystemSource' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Template' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Mustache_Tokenizer' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
|
||||
'Nette\\ArgumentOutOfRangeException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
|
||||
'Nette\\DeprecatedException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
|
||||
'Nette\\DirectoryNotFoundException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"packages": [
|
||||
[
|
||||
[
|
||||
{
|
||||
"name": "composer/ca-bundle",
|
||||
"version": "1.5.13",
|
||||
@@ -718,62 +718,6 @@
|
||||
},
|
||||
"install-path": "../maxmind/web-service-common"
|
||||
},
|
||||
{
|
||||
"name": "mustache/mustache",
|
||||
"version": "v3.0.0",
|
||||
"version_normalized": "3.0.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bobthecow/mustache.php.git",
|
||||
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/176b6b21d68516dd5107a63ab71b0050e518b7a4",
|
||||
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "~2.19.3",
|
||||
"yoast/phpunit-polyfills": "^2.0"
|
||||
},
|
||||
"time": "2025-06-28T18:28:20+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Mustache\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"src/compat.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Justin Hileman",
|
||||
"email": "justin@justinhileman.info",
|
||||
"homepage": "http://justinhileman.com"
|
||||
}
|
||||
],
|
||||
"description": "A Mustache implementation in PHP.",
|
||||
"homepage": "https://github.com/bobthecow/mustache.php",
|
||||
"keywords": [
|
||||
"mustache",
|
||||
"templating"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/bobthecow/mustache.php/issues",
|
||||
"source": "https://github.com/bobthecow/mustache.php/tree/v3.0.0"
|
||||
},
|
||||
"install-path": "../mustache/mustache"
|
||||
},
|
||||
{
|
||||
"name": "myclabs/php-enum",
|
||||
"version": "1.8.5",
|
||||
@@ -1000,66 +944,6 @@
|
||||
},
|
||||
"install-path": "../nette/utils"
|
||||
},
|
||||
{
|
||||
"name": "php-mqtt/client",
|
||||
"version": "v2.3.0",
|
||||
"version_normalized": "2.3.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-mqtt/client.git",
|
||||
"reference": "3d141846753a0adee265680ae073cfb9030f2390"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-mqtt/client/zipball/3d141846753a0adee265680ae073cfb9030f2390",
|
||||
"reference": "3d141846753a0adee265680ae073cfb9030f2390",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"myclabs/php-enum": "^1.7",
|
||||
"php": "^8.0",
|
||||
"psr/log": "^1.1|^2.0|^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/php-invoker": "^3.0",
|
||||
"phpunit/phpunit": "^9.0",
|
||||
"squizlabs/php_codesniffer": "^3.5"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-redis": "Required for the RedisRepository"
|
||||
},
|
||||
"time": "2025-09-30T17:53:34+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhpMqtt\\Client\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Marvin Mall",
|
||||
"email": "marvin-mall@msn.com",
|
||||
"role": "developer"
|
||||
}
|
||||
],
|
||||
"description": "An MQTT client written in and for PHP.",
|
||||
"keywords": [
|
||||
"client",
|
||||
"mqtt",
|
||||
"publish",
|
||||
"subscribe"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-mqtt/client/issues",
|
||||
"source": "https://github.com/php-mqtt/client/tree/v2.3.0"
|
||||
},
|
||||
"install-path": "../php-mqtt/client"
|
||||
},
|
||||
{
|
||||
"name": "psr/event-dispatcher",
|
||||
"version": "1.0.0",
|
||||
@@ -1906,6 +1790,6 @@
|
||||
"install-path": "../twig/twig"
|
||||
}
|
||||
],
|
||||
"dev": true,
|
||||
"dev-package-names": []
|
||||
}
|
||||
true,
|
||||
[]
|
||||
]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => '__root__',
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => 'd453b8073f07afc24f2a03f94a982977263aa107',
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
@@ -11,9 +11,9 @@
|
||||
),
|
||||
'versions' => array(
|
||||
'__root__' => array(
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => 'd453b8073f07afc24f2a03f94a982977263aa107',
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
@@ -100,15 +100,6 @@
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'mustache/mustache' => array(
|
||||
'pretty_version' => 'v3.0.0',
|
||||
'version' => '3.0.0.0',
|
||||
'reference' => '176b6b21d68516dd5107a63ab71b0050e518b7a4',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../mustache/mustache',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'myclabs/php-enum' => array(
|
||||
'pretty_version' => '1.8.5',
|
||||
'version' => '1.8.5.0',
|
||||
@@ -136,15 +127,6 @@
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'php-mqtt/client' => array(
|
||||
'pretty_version' => 'v2.3.0',
|
||||
'version' => '2.3.0.0',
|
||||
'reference' => '3d141846753a0adee265680ae073cfb9030f2390',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../php-mqtt/client',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/event-dispatcher' => array(
|
||||
'pretty_version' => '1.0.0',
|
||||
'version' => '1.0.0.0',
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
return [
|
||||
'version' => '2.6.0',
|
||||
'release_date' => '2026-08-15',
|
||||
'codename' => 'Atlas',
|
||||
'version' => '2.6.1',
|
||||
'release_date' => '2026-08-17',
|
||||
'codename' => 'Lyra',
|
||||
'status' => 'stable',
|
||||
|
||||
'system_requirements' => [
|
||||
|
||||