Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0e6e28dcc | ||
|
|
616b23ce77 | ||
|
|
b38be8366c | ||
|
|
c2bcd7be22 | ||
|
|
6daa0a6f49 | ||
|
|
73450a17c1 | ||
|
|
8ebfcb6061 | ||
|
|
b6896c60e5 | ||
|
|
d733e26f91 | ||
|
|
4e369d887b | ||
|
|
e8e3c97ccd | ||
|
|
8ebf11d7e4 | ||
|
|
3d84e61ed1 | ||
|
|
0137877439 | ||
|
|
46c653eb21 | ||
|
|
2d61f9bab6 | ||
|
|
e9d2ec64bb | ||
|
|
8a0637eddc | ||
|
|
d84a2989d4 | ||
|
|
6bdd5faa7a | ||
|
|
b495fc9ab0 | ||
|
|
2c0e62bbae |
@@ -21,6 +21,9 @@ admin/storage/geoip/
|
|||||||
admin/storage/stats.json
|
admin/storage/stats.json
|
||||||
var/
|
var/
|
||||||
|
|
||||||
|
# Runtime-compiled theme assets (generated by scssphp, read-only)
|
||||||
|
themes/*/assets/css_compiled/
|
||||||
|
|
||||||
# Runtime-compiled theme assets
|
# Runtime-compiled theme assets
|
||||||
public/themes/
|
public/themes/
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
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') }}
|
<i class="bi bi-gear-fill"></i> {{ ta.admin_title|default('CodePress Admin') }}
|
||||||
</div>
|
</div>
|
||||||
<ul class="nav flex-column mt-2">
|
<ul class="nav flex-column mt-2">
|
||||||
{# Algemene sectie: Dashboard (plugin) + plugin items met section=general #}
|
{# Algemene sectie: Dashboard (altijd zichtbaar) + plugin items met section=general #}
|
||||||
{% set general_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'general') %}
|
{% set general_plugins = plugin_admin_menu|filter(item => (item.section|default('general')) == 'general' and item.route != 'dashboard') %}
|
||||||
{% if general_plugins is not empty or has_permission('dashboard') or true %}
|
|
||||||
<li class="nav-section">{{ ta.section_general|default('Algemeen') }}</li>
|
<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 %}
|
{% for item in general_plugins %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {{ route == item.route or route starts with item.route ~ '/' ? 'active' : '' }}" href="/admin/{{ item.route }}">
|
<a class="nav-link {{ route == item.route or route starts with item.route ~ '/' ? 'active' : '' }}" href="/admin/{{ item.route }}">
|
||||||
@@ -51,7 +55,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if has_permission('content') %}
|
{% if has_permission('content') %}
|
||||||
<li class="nav-section">{{ ta.section_content|default('Content') }}</li>
|
<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>
|
<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>
|
<small class="form-text text-muted">{{ ta.plugin_name_help|default('Alleen letters, cijfers, underscores en streepjes.') }}</small>
|
||||||
</div>
|
</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>
|
||||||
<div class="card-footer bg-white">
|
<div class="card-footer bg-white">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
|
|||||||
@@ -1203,9 +1203,22 @@ class CodePressCMS {
|
|||||||
$content .= '<p>' . $this->t('directory_empty') . '.</p>';
|
$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 [
|
return [
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
'content' => $content
|
'content' => $content,
|
||||||
|
'layout' => $layout,
|
||||||
|
'metadata' => $metadata,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1489,7 +1502,7 @@ class CodePressCMS {
|
|||||||
$isExpanded = $this->folderContainsActivePage($item['children']);
|
$isExpanded = $this->folderContainsActivePage($item['children']);
|
||||||
|
|
||||||
if ($level === 0) {
|
if ($level === 0) {
|
||||||
// Root level folders
|
// Root level folders - Bootstrap dropdown
|
||||||
$html .= '<li class="nav-item 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 .= '<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>';
|
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>';
|
||||||
@@ -1499,12 +1512,12 @@ class CodePressCMS {
|
|||||||
$html .= '</ul>';
|
$html .= '</ul>';
|
||||||
$html .= '</li>';
|
$html .= '</li>';
|
||||||
} else {
|
} else {
|
||||||
// Nested folders in dropdown
|
// Nested folders - CSS hover submenu (unlimited depth)
|
||||||
$html .= '<li class="dropdown-submenu">';
|
$html .= '<li class="dropdown-submenu' . ($isExpanded ? ' show' : '') . '">';
|
||||||
$html .= '<a class="dropdown-item dropdown-toggle" href="#" id="' . $folderId . '">';
|
$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-down"></i>';
|
$html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-right"></i>';
|
||||||
$html .= '</a>';
|
$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 .= $this->renderMenu($item['children'], $level + 1);
|
||||||
$html .= '</ul>';
|
$html .= '</ul>';
|
||||||
$html .= '</li>';
|
$html .= '</li>';
|
||||||
|
|||||||
@@ -169,8 +169,22 @@ class ThemeManager {
|
|||||||
$compiler = new Compiler();
|
$compiler = new Compiler();
|
||||||
$compiler->setImportPaths($this->themeDir . '/assets/scss');
|
$compiler->setImportPaths($this->themeDir . '/assets/scss');
|
||||||
$css = $compiler->compileString(file_get_contents($scssFile))->getCss();
|
$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($outFile, $css);
|
||||||
file_put_contents($cacheFile, (string)$mtime);
|
file_put_contents($cacheFile, (string)$mtime);
|
||||||
|
|
||||||
|
// Set read-only to prevent manual edits
|
||||||
|
@chmod($outFile, 0444);
|
||||||
|
@chmod($cacheFile, 0444);
|
||||||
|
|
||||||
return $outFile;
|
return $outFile;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
error_log('ThemeManager SCSS compile error: ' . $e->getMessage());
|
error_log('ThemeManager SCSS compile error: ' . $e->getMessage());
|
||||||
@@ -180,16 +194,11 @@ class ThemeManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the public URL for the theme CSS.
|
* 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 {
|
public function getCssUrl(): ?string {
|
||||||
$manualCss = $this->themeDir . '/assets/css/theme.css';
|
|
||||||
$compiledCss = $this->themeDir . '/assets/css_compiled/theme.css';
|
$compiledCss = $this->themeDir . '/assets/css_compiled/theme.css';
|
||||||
|
|
||||||
if (is_file($manualCss)) {
|
|
||||||
return $this->getThemeAssetUrl('css/theme.css');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_file($compiledCss)) {
|
if (is_file($compiledCss)) {
|
||||||
return $this->getThemeAssetUrl('css_compiled/theme.css');
|
return $this->getThemeAssetUrl('css_compiled/theme.css');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
The admin manager guide contains the following topics:
|
The admin manager guide contains the following topics:
|
||||||
|
|
||||||
- **Dashboard** - Website overview
|
- **Dashboard** - Role-based overview of the website (Admin, Content Manager, BI Manager, Site Admin)
|
||||||
- **Content management** - Managing files and pages
|
- **Content management** - Managing files and pages with layout selection and plugins
|
||||||
- **Configuration** - Site settings
|
- **Configuration** - Site settings (title, language, author, analytics, logging)
|
||||||
- **Theme management** - Managing and creating themes
|
- **Theme management** - Managing, activating and creating themes (SCSS compilation)
|
||||||
- **Security** - Bot protection and sessions
|
- **Security** - Bot protection, rate limiting and session settings
|
||||||
- **Plugins** - Managing and creating plugins
|
- **Plugins** - Managing plugins: content (sidebar) and system (admin menu/API)
|
||||||
- **Users** - Adding and removing users
|
- **Users** - User management with roles (Admin, Content Manager, BI Manager, Site Admin)
|
||||||
- **Statistics** - Viewing visitor statistics
|
- **Statistics** - Viewing and exporting visitor statistics
|
||||||
- **Logs** - Viewing and filtering log files
|
- **Logs** - Viewing and filtering log files
|
||||||
- **Media** - Managing media files
|
- **Media** - Managing media files
|
||||||
- **Update** - Check for updates
|
- **Update** - Check for updates
|
||||||
|
|||||||
@@ -2,16 +2,45 @@
|
|||||||
|
|
||||||
## Managing files
|
## Managing files
|
||||||
|
|
||||||
- **Upload** - Upload media files
|
- **New folder** — Create folder structure
|
||||||
- **New folder** - Create folder structure
|
- **New file** — Create a page (`.md`, `.php`, `.html`)
|
||||||
- **New file** - Create a page
|
- **Edit** — Modify existing content in the CodeMirror editor
|
||||||
- **Edit** - Modify existing content
|
- **Rename** — Change file or folder names
|
||||||
- **Rename** - Change file names
|
- **Move** — Move content to another folder
|
||||||
- **Move** - Move content
|
- **Delete** — Remove content
|
||||||
- **Delete** - Remove content
|
- **Rename folder** — Change a folder name
|
||||||
|
|
||||||
## Editor
|
## Editor (content-edit)
|
||||||
|
|
||||||
- CodeMirror with syntax highlighting
|
- **CodeMirror** with syntax highlighting (Markdown, PHP, HTML)
|
||||||
- Toolbar for Markdown formatting
|
- **Toolbar** for quickly inserting Markdown
|
||||||
- Shortcuts: Ctrl+S (save), Ctrl+N (new)
|
- **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
|
# 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)
|
## Roles and widgets
|
||||||
- Unique visitors
|
|
||||||
|
### Admin
|
||||||
|
- Views (30 days) and unique visitors
|
||||||
- Number of pages and folders
|
- Number of pages and folders
|
||||||
- Active plugins
|
- Active plugins
|
||||||
- Recent activity
|
- 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
|
# 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**
|
1. Go to **Users**
|
||||||
2. Enter username
|
2. Enter username
|
||||||
3. Choose password
|
3. Choose password (stored as bcrypt hash)
|
||||||
4. Click **Add**
|
4. Select a role
|
||||||
|
5. Click **Add**
|
||||||
|
|
||||||
## Delete user
|
## Editing a user
|
||||||
|
|
||||||
- Cannot delete own account
|
- 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
|
- 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
|
## Managing themes
|
||||||
|
|
||||||
1. Go to **Theme** in admin menu
|
1. Go to **Theme** in the admin menu
|
||||||
2. **Activate** - Choose active theme
|
2. **Activate** — Choose the active theme (stored in `config.json`)
|
||||||
3. **Compile SCSS** - Process SCSS to CSS
|
3. **Compile SCSS** — Process SCSS to CSS (forced)
|
||||||
4. **New theme** - Create custom theme
|
4. **New theme** — Create a custom theme via admin or manually
|
||||||
|
|
||||||
## Theme structure
|
## Theme structure
|
||||||
|
|
||||||
```
|
```
|
||||||
themes/default/
|
themes/default/
|
||||||
├── theme.json # Theme configuration
|
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||||
├── base.twig # Main layout
|
├── base.twig # Main layout (head, header, nav, breadcrumb, footer)
|
||||||
├── full_content.twig # Layouts
|
├── full_content.twig # Layout: full width
|
||||||
├── left_sidebar.twig
|
├── left_sidebar.twig # Layout: sidebar on the left
|
||||||
├── right_sidebar.twig
|
├── right_sidebar.twig # Layout: sidebar on the right
|
||||||
├── partials/ # Header, nav, footer
|
├── custom1.twig # Layout: custom
|
||||||
└── assets/ # CSS, JS, images
|
├── 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:
|
The CodePress developer guide contains the following topics:
|
||||||
|
|
||||||
- **Architecture** - CMS architecture and folder structure
|
- **Architecture** — CMS architecture and folder structure
|
||||||
- **Core classes** - CodePressCMS, ThemeManager, PluginManager
|
- **Core classes** — CodePressCMS, ThemeManager, PluginManager, AdminAuth
|
||||||
- **Plugin development** - Developing plugins
|
- **Plugin development** — Plugin types (content/system), structure, API
|
||||||
- **Routing** - Frontend and admin routing
|
- **Routing** — Frontend, admin and plugin admin routing
|
||||||
- **Security** - XSS, CSRF, path traversal
|
- **Security** — XSS, CSRF, path traversal, RBAC
|
||||||
- **Testing** - Penetration, accessibility and functional tests
|
- **Testing** — Penetration, accessibility and functional tests
|
||||||
- **Debugging** - Logging and cache
|
- **Debugging** — Logging and cache
|
||||||
- **Performance** - OPcache and SCSS caching
|
- **Performance** — OPcache and SCSS caching
|
||||||
|
|
||||||
|
Important concepts in CodePress 2.5.2:
|
||||||
|
|
||||||
|
- **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.
|
Select a topic from the navigation on the left.
|
||||||
@@ -2,18 +2,60 @@
|
|||||||
|
|
||||||
## Frontend routing
|
## 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
|
### PHP dev server
|
||||||
// Clean URLs: /nl/page
|
|
||||||
// Query: ?page=page&lang=nl
|
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
|
## 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
|
```php
|
||||||
// Routes: /admin/dashboard, /admin/content, etc.
|
// PluginManager calls this for /admin/my-system
|
||||||
// Query parameter: ?route=dashboard
|
$plugin->handleAdminRoute('my-system');
|
||||||
```
|
```
|
||||||
@@ -4,28 +4,48 @@ Content is stored in the `content/` folder without a database.
|
|||||||
|
|
||||||
## Supported file formats
|
## Supported file formats
|
||||||
|
|
||||||
- `.md` - Markdown (recommended)
|
- `.md` — Markdown (recommended)
|
||||||
- `.php` - Dynamic PHP pages
|
- `.php` — Dynamic PHP pages
|
||||||
- `.html` - Static HTML pages
|
- `.html` — Static HTML pages
|
||||||
|
|
||||||
## File name conventions
|
## 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
|
## Frontmatter
|
||||||
|
|
||||||
Markdown files can contain frontmatter metadata:
|
Markdown files can contain frontmatter metadata:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
---
|
---
|
||||||
layout: full_content
|
layout: left_sidebar
|
||||||
plugins: HTMLBlock
|
plugins:
|
||||||
|
- HTMLBlock
|
||||||
|
- Navigation
|
||||||
---
|
---
|
||||||
|
|
||||||
# Page title
|
# Page title
|
||||||
|
|
||||||
Content...
|
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`
|
1. Login at `/admin`
|
||||||
2. Go to **Content**
|
2. Go to **Content**
|
||||||
3. Choose a folder or create a new page
|
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
|
5. Save with Ctrl+S
|
||||||
|
|
||||||
## Editor features
|
## Editor features
|
||||||
|
|
||||||
- **CodeMirror** editor with syntax highlighting
|
- **CodeMirror** editor with syntax highlighting (Markdown, PHP, HTML)
|
||||||
- **Toolbar** for quick Markdown insertion
|
- **Toolbar** for quick Markdown insertion
|
||||||
- **Live preview** via button
|
- **Shortcuts**: Ctrl+S (save), Ctrl+N (new)
|
||||||
- **Auto-save** backups in `.bak/` folder
|
- **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
|
# Manual
|
||||||
|
|
||||||
|
Welcome to the CodePress CMS manual (version 2.5.2). 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.
|
Select a topic from the navigation on the left.
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
The theme developer guide contains the following topics:
|
The theme developer guide contains the following topics:
|
||||||
|
|
||||||
- **Theme structure** - Folder structure of a theme
|
- **Theme structure** - Folder structure of a theme
|
||||||
- **theme.json** - Theme configuration
|
- **theme.json** - Configuration of a theme
|
||||||
- **Twig templates** - Twig syntax, variables and blocks
|
- **Twig templates** - Twig syntax, variables and blocks
|
||||||
- **SCSS styling** - Compiling and writing SCSS
|
- **SCSS styling** - Compiling and writing SCSS
|
||||||
- **Layouts** - Defining and using layouts
|
- **Layouts** - Defining and using layouts
|
||||||
|
|||||||
@@ -2,20 +2,49 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
themes/my-theme/
|
themes/my-theme/
|
||||||
├── theme.json # Theme configuration
|
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||||
├── base.twig # Main layout
|
├── base.twig # Main layout (head, header, nav, breadcrumb, footer)
|
||||||
├── full_content.twig # Layout: full-width
|
├── full_content.twig # Layout: full width
|
||||||
├── left_sidebar.twig # Layout: left sidebar
|
├── left_sidebar.twig # Layout: sidebar on the left
|
||||||
├── right_sidebar.twig # Layout: right sidebar
|
├── right_sidebar.twig # Layout: sidebar on the right
|
||||||
├── custom.twig # Layout: custom
|
├── custom1.twig # Layout: custom
|
||||||
|
├── guide.twig # Layout: guide with sidebar (Navigation plugin)
|
||||||
├── partials/
|
├── partials/
|
||||||
│ ├── header.twig
|
│ ├── header.twig
|
||||||
│ ├── navigation.twig
|
│ ├── navigation.twig
|
||||||
│ └── footer.twig
|
│ └── footer.twig
|
||||||
└── assets/
|
└── assets/
|
||||||
├── scss/theme.scss # SCSS source
|
├── scss/theme.scss # SCSS source (the only CSS source)
|
||||||
├── css/ # CSS files
|
├── css_compiled/ # Generated by scssphp (read-only, do not edit manually)
|
||||||
├── js/theme.js # JavaScript
|
├── css/ # External CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
|
||||||
├── fonts/ # Fonts
|
├── js/ # app.js, bootstrap.bundle.min.js
|
||||||
└── img/ # Images
|
├── 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:
|
De admin beheerder handleiding bevat de volgende onderwerpen:
|
||||||
|
|
||||||
- **Dashboard** - Overzicht van de website
|
- **Dashboard** - Role-based overzicht van de website (Admin, Content Beheerder, BI Beheerder, Site Admin)
|
||||||
- **Content beheer** - Bestanden en pagina's beheren
|
- **Content beheer** - Bestanden en pagina's beheren met layout selectie en plugins
|
||||||
- **Configuratie** - Site instellingen
|
- **Configuratie** - Site instellingen (titel, taal, auteur, analytics, logging)
|
||||||
- **Thema beheer** - Thema's beheren en aanmaken
|
- **Thema beheer** - Thema's beheren, activeren en aanmaken (SCSS compilatie)
|
||||||
- **Beveiliging** - Bot bescherming en sessies
|
- **Beveiliging** - Bot bescherming, rate limiting en sessie instellingen
|
||||||
- **Plugins** - Plugins beheren en aanmaken
|
- **Plugins** - Plugins beheren: content (sidebar) en systeem (admin menu/API)
|
||||||
- **Gebruikers** - Gebruikers toevoegen en verwijderen
|
- **Gebruikers** - Gebruikersbeheer met rollen (Admin, Content Beheerder, BI Beheerder, Site Admin)
|
||||||
- **Statistieken** - Bezoekersstatistieken bekijken
|
- **Statistieken** - Bezoekersstatistieken bekijken en exporteren
|
||||||
- **Logs** - Logbestanden bekijken en filteren
|
- **Logs** - Logbestanden bekijken en filteren
|
||||||
- **Media** - Media bestanden beheren
|
- **Media** - Media bestanden beheren
|
||||||
- **Update** - Controleren op updates
|
- **Update** - Controleren op updates
|
||||||
|
|||||||
@@ -2,16 +2,45 @@
|
|||||||
|
|
||||||
## Bestanden beheren
|
## Bestanden beheren
|
||||||
|
|
||||||
- **Uploaden** - Media bestanden uploaden
|
- **Nieuwe map** — Mappen structuur aanmaken
|
||||||
- **Nieuwe map** - Mappen structuur aanmaken
|
- **Nieuw bestand** — Pagina aanmaken (`.md`, `.php`, `.html`)
|
||||||
- **Nieuw bestand** - Pagina aanmaken
|
- **Bewerken** — Bestaande content wijzigen in CodeMirror editor
|
||||||
- **Bewerken** - Bestaande content wijzigen
|
- **Hernoemen** — Bestands- of mapnamen aanpassen
|
||||||
- **Hernoemen** - Bestandsnamen aanpassen
|
- **Verplaatsen** — Content verplaatsen naar andere map
|
||||||
- **Verplaatsen** - Content verplaatsen
|
- **Verwijderen** — Content verwijderen
|
||||||
- **Verwijderen** - Content verwijderen
|
- **Map hernoemen** — Map naam wijzigen
|
||||||
|
|
||||||
## Editor
|
## Editor (content-edit)
|
||||||
|
|
||||||
- CodeMirror met syntax highlighting
|
- **CodeMirror** met syntax highlighting (Markdown, PHP, HTML)
|
||||||
- Toolbar voor Markdown formatting
|
- **Toolbar** voor snel Markdown invoeren
|
||||||
- Sneltoetsen: Ctrl+S (opslaan), Ctrl+N (nieuw)
|
- **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
|
# 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)
|
## Rollen en widgets
|
||||||
- Unieke bezoekers
|
|
||||||
|
### Admin
|
||||||
|
- Weergaven (30 dagen) en unieke bezoekers
|
||||||
- Aantal pagina's en mappen
|
- Aantal pagina's en mappen
|
||||||
- Actieve plugins
|
- Actieve plugins
|
||||||
- Recente activiteit
|
- 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
|
||||||
|
|
||||||
|
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
|
## Gebruiker toevoegen
|
||||||
|
|
||||||
1. Ga naar **Gebruikers**
|
1. Ga naar **Gebruikers**
|
||||||
2. Vul gebruikersnaam in
|
2. Vul gebruikersnaam in
|
||||||
3. Kies wachtwoord
|
3. Kies wachtwoord (opgeslagen als bcrypt hash)
|
||||||
4. Klik **Toevoegen**
|
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
|
## Gebruiker verwijderen
|
||||||
|
|
||||||
- Kan niet voor eigen account
|
- 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
|
## Thema's beheren
|
||||||
|
|
||||||
1. Ga naar **Thema** in admin menu
|
1. Ga naar **Thema** in admin menu
|
||||||
2. **Activeren** - Kies actief thema
|
2. **Activeren** — Kies actief thema (wordt opgeslagen in `config.json`)
|
||||||
3. **SCSS compileren** - Verwerk SCSS naar CSS
|
3. **SCSS compileren** — Verwerk SCSS naar CSS (geforceerd)
|
||||||
4. **Nieuw thema** - Eigen thema aanmaken
|
4. **Nieuw thema** — Eigen thema aanmaken via admin of handmatig
|
||||||
|
|
||||||
## Thema structuur
|
## Thema structuur
|
||||||
|
|
||||||
```
|
```
|
||||||
themes/default/
|
themes/default/
|
||||||
├── theme.json # Thema configuratie
|
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||||
├── base.twig # Hoofd layout
|
├── base.twig # Hoofd layout (head, header, nav, breadcrumb, footer)
|
||||||
├── full_content.twig # Layouts
|
├── full_content.twig # Layout: volledige breedte
|
||||||
├── partials/ # Header, nav, footer
|
├── left_sidebar.twig # Layout: sidebar links
|
||||||
└── assets/ # CSS, JS, afbeeldingen
|
├── 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:
|
De CodePress developer handleiding bevat de volgende onderwerpen:
|
||||||
|
|
||||||
- **Architectuur** - CMS architectuur en mappenstructuur
|
- **Architectuur** — CMS architectuur en mappenstructuur
|
||||||
- **Core classes** - CodePressCMS, ThemeManager, PluginManager
|
- **Core classes** — CodePressCMS, ThemeManager, PluginManager, AdminAuth
|
||||||
- **Plugin development** - Plugins ontwikkelen
|
- **Plugin development** — Plugin types (content/systeem), structuur, API
|
||||||
- **Routing** - Frontend en admin routing
|
- **Routing** — Frontend, admin en plugin admin routing
|
||||||
- **Beveiliging** - XSS, CSRF, path traversal
|
- **Beveiliging** — XSS, CSRF, path traversal, RBAC
|
||||||
- **Testing** - Penetratie, accessibility en functionele tests
|
- **Testing** — Penetratie, accessibility en functionele tests
|
||||||
- **Debugging** - Logging en cache
|
- **Debugging** — Logging en cache
|
||||||
- **Performance** - OPcache en SCSS caching
|
- **Performance** — OPcache en SCSS caching
|
||||||
|
|
||||||
|
Belangrijke concepten in CodePress 2.5.2:
|
||||||
|
|
||||||
|
- **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.
|
Selecteer een onderwerp uit de navigatie aan de linkerkant.
|
||||||
@@ -2,18 +2,60 @@
|
|||||||
|
|
||||||
## Frontend routing
|
## 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
|
### PHP dev server
|
||||||
// Schone URLs: /nl/pagina
|
|
||||||
// Query: ?page=pagina&lang=nl
|
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
|
## 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
|
```php
|
||||||
// Routes: /admin/dashboard, /admin/content, etc.
|
// PluginManager roept deze aan voor /admin/mijn-systeem
|
||||||
// Query parameter: ?route=dashboard
|
$plugin->handleAdminRoute('mijn-systeem');
|
||||||
```
|
```
|
||||||
@@ -4,28 +4,48 @@ Content wordt opgeslagen in de `content/` map zonder database.
|
|||||||
|
|
||||||
## Ondersteunde bestandsformaten
|
## Ondersteunde bestandsformaten
|
||||||
|
|
||||||
- `.md` - Markdown (aanbevolen)
|
- `.md` — Markdown (aanbevolen)
|
||||||
- `.php` - Dynamische PHP pagina's
|
- `.php` — Dynamische PHP pagina's
|
||||||
- `.html` - Statische HTML pagina's
|
- `.html` — Statische HTML pagina's
|
||||||
|
|
||||||
## Bestandsnaam conventies
|
## Bestandsnaam conventies
|
||||||
|
|
||||||
```
|
```
|
||||||
nl.pagina-naam.md # Nederlandse pagina
|
nl.pagina-naam.md # Nederlandse pagina
|
||||||
en.page-name.md # Engelse 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
|
## Frontmatter
|
||||||
|
|
||||||
Markdown bestanden kunnen frontmatter metadata bevatten:
|
Markdown bestanden kunnen frontmatter metadata bevatten:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
---
|
---
|
||||||
layout: full_content
|
layout: left_sidebar
|
||||||
plugins: HTMLBlock
|
plugins:
|
||||||
|
- HTMLBlock
|
||||||
|
- Navigation
|
||||||
---
|
---
|
||||||
|
|
||||||
# Pagina titel
|
# Pagina titel
|
||||||
|
|
||||||
Content...
|
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`
|
1. Login op `/admin`
|
||||||
2. Ga naar **Content**
|
2. Ga naar **Content**
|
||||||
3. Kies een map of maak een nieuwe pagina
|
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
|
5. Sla op met Ctrl+S
|
||||||
|
|
||||||
## Editor functies
|
## Editor functies
|
||||||
|
|
||||||
- **CodeMirror** editor met syntax highlighting
|
- **CodeMirror** editor met syntax highlighting (Markdown, PHP, HTML)
|
||||||
- **Toolbar** voor snel Markdown invoegen
|
- **Toolbar** voor snel Markdown invoegen
|
||||||
- **Live preview** via knop
|
- **Sneltoetsen**: Ctrl+S (opslaan), Ctrl+N (nieuw)
|
||||||
- **Auto-save** backups in `.bak/` map
|
- **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
|
# Handleiding
|
||||||
|
|
||||||
|
Welkom bij de CodePress CMS handleiding (versie 2.5.2). 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.
|
Selecteer een onderwerp uit de navigatie aan de linkerkant.
|
||||||
@@ -2,20 +2,49 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
themes/mijn-thema/
|
themes/mijn-thema/
|
||||||
├── theme.json # Thema configuratie
|
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||||
├── base.twig # Hoofd layout
|
├── base.twig # Hoofd layout (head, header, nav, breadcrumb, footer)
|
||||||
├── full_content.twig # Layout: full-width
|
├── full_content.twig # Layout: volledige breedte
|
||||||
├── left_sidebar.twig # Layout: sidebar links
|
├── left_sidebar.twig # Layout: sidebar links
|
||||||
├── right_sidebar.twig # Layout: sidebar rechts
|
├── right_sidebar.twig # Layout: sidebar rechts
|
||||||
├── custom.twig # Layout: custom
|
├── custom1.twig # Layout: custom
|
||||||
|
├── guide.twig # Layout: handleiding met sidebar (Navigation plugin)
|
||||||
├── partials/
|
├── partials/
|
||||||
│ ├── header.twig
|
│ ├── header.twig
|
||||||
│ ├── navigation.twig
|
│ ├── navigation.twig
|
||||||
│ └── footer.twig
|
│ └── footer.twig
|
||||||
└── assets/
|
└── assets/
|
||||||
├── scss/theme.scss # SCSS bron
|
├── scss/theme.scss # SCSS bron (de enige CSS bron)
|
||||||
├── css/ # CSS bestanden
|
├── css_compiled/ # Gegenereerd door scssphp (read-only, niet handmatig aanpassen)
|
||||||
├── js/theme.js # JavaScript
|
├── css/ # Externe CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
|
||||||
├── fonts/ # Lettertypes
|
├── js/ # app.js, bootstrap.bundle.min.js
|
||||||
└── img/ # Afbeeldingen
|
├── 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 %}
|
||||||
```
|
```
|
||||||
@@ -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';
|
$currentPage = $this->api ? $this->api->getCurrentPageTitle() : 'Onbekend';
|
||||||
$isHomepage = $this->api ? $this->api->isHomepage() : false;
|
$isHomepage = $this->api ? $this->api->isHomepage() : false;
|
||||||
$currentLang = $this->api ? $this->api->getCurrentLanguage() : 'nl';
|
$currentLang = $this->api ? $this->api->getCurrentLanguage() : 'nl';
|
||||||
|
$currentPath = $_GET['page'] ?? '';
|
||||||
|
$currentPath = preg_replace('/\.(md|php|html)$/', '', $currentPath);
|
||||||
|
|
||||||
$content = '
|
$content = '
|
||||||
<p class="mb-2"><strong>Huidige pagina:</strong> ' . htmlspecialchars($currentPage) . '</p>
|
<p class="mb-2"><strong>Huidige pagina:</strong> ' . htmlspecialchars($currentPage) . '</p>
|
||||||
@@ -43,7 +45,7 @@ class HTMLBlock
|
|||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add quick navigation
|
// Add dynamic quick navigation
|
||||||
$menu = $this->api->getMenu();
|
$menu = $this->api->getMenu();
|
||||||
if (!empty($menu)) {
|
if (!empty($menu)) {
|
||||||
$content .= '
|
$content .= '
|
||||||
@@ -52,8 +54,25 @@ class HTMLBlock
|
|||||||
|
|
||||||
foreach ($menu as $item) {
|
foreach ($menu as $item) {
|
||||||
if ($item['type'] === 'file') {
|
if ($item['type'] === 'file') {
|
||||||
$url = $this->api->createUrl($item['path']);
|
$url = '/' . $currentLang . '/' . $item['path'];
|
||||||
$content .= '<li><a href="' . htmlspecialchars($url) . '" class="text-decoration-none">📄 ' . htmlspecialchars($item['title']) . '</a></li>';
|
$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>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -18,7 +18,16 @@ Options -Indexes
|
|||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
RewriteBase /
|
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=dashboard [L,QSA]
|
||||||
RewriteRule ^admin/(.+)$ admin.php?route=$1 [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,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;
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
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) {
|
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) {
|
if (toggle) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const submenu = toggle.closest('.dropdown-submenu');
|
const submenu = toggle.parentElement;
|
||||||
const dropdown = submenu.querySelector('.dropdown-menu');
|
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;
|
const parent = submenu.parentElement;
|
||||||
parent.querySelectorAll('.dropdown-submenu').forEach(function(sibling) {
|
parent.querySelectorAll(':scope > .dropdown-submenu').forEach(function(sibling) {
|
||||||
if (sibling !== submenu) {
|
if (sibling !== submenu) {
|
||||||
|
sibling.classList.remove('show');
|
||||||
var siblingMenu = sibling.querySelector('.dropdown-menu');
|
var siblingMenu = sibling.querySelector('.dropdown-menu');
|
||||||
if (siblingMenu) siblingMenu.classList.remove('show');
|
if (siblingMenu) siblingMenu.classList.remove('show');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle current submenu
|
// Toggle current submenu
|
||||||
if (dropdown) dropdown.classList.toggle('show');
|
if (dropdown) {
|
||||||
|
dropdown.classList.toggle('show');
|
||||||
|
submenu.classList.toggle('show');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close all open submenus when clicking outside
|
// Close all open submenus when clicking outside
|
||||||
document.querySelectorAll('.dropdown-submenu .dropdown-menu.show').forEach(function(menu) {
|
document.querySelectorAll('.dropdown-submenu.show').forEach(function(sm) {
|
||||||
menu.classList.remove('show');
|
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 {
|
.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;
|
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 {
|
.dropdown-item {
|
||||||
color: var(--header-font) !important;
|
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;
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
color: var(--header-font) !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
|
// Hide Bootstrap dropdown arrow and use custom icon
|
||||||
.dropdown-toggle::after {
|
.dropdown-toggle::after {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@@ -217,8 +239,7 @@ $header-bg-opacity: 1;
|
|||||||
// Enhanced accessibility styles
|
// Enhanced accessibility styles
|
||||||
.focus-visible:focus,
|
.focus-visible:focus,
|
||||||
.btn:focus,
|
.btn:focus,
|
||||||
.form-control:focus,
|
.form-control:focus {
|
||||||
.nav-link:focus {
|
|
||||||
outline: 3px solid #0056b3 !important;
|
outline: 3px solid #0056b3 !important;
|
||||||
outline-offset: 2px !important;
|
outline-offset: 2px !important;
|
||||||
box-shadow: 0 0 0 1px #ffffff, 0 0 0 4px #0056b3 !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 {
|
.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;
|
background-color: transparent !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
|
border-bottom: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
gap: 0 !important;
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
border: none !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;
|
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;
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
border: none !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||||
|
border: none !important;
|
||||||
|
border-color: transparent !important;
|
||||||
border-bottom: 2px solid var(--nav-font) !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
|
// Sidebar styling
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<link rel="me" href="{{ author_git }}">
|
<link rel="me" href="{{ author_git }}">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- 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="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="apple-touch-icon" href="{{ theme_base_url }}/assets/img/favicon.svg">
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
Reference in New Issue
Block a user