+= module_shell_footer() ?>
diff --git a/modules/pihole/pages/instances.php b/modules/pihole/pages/instances.php
index 2ea1807..1ed3290 100644
--- a/modules/pihole/pages/instances.php
+++ b/modules/pihole/pages/instances.php
@@ -132,15 +132,23 @@ if ($primaryId === '') {
}
}
?>
+= module_shell_header('pihole', [
+ 'title' => 'Pi-hole Instanzen',
+ 'description' => 'Pi-hole Instanzen hinzufuegen, bearbeiten und loeschen.',
+ 'tabs' => [
+ ['label' => 'Dashboard', 'href' => '/module/pihole'],
+ ['label' => 'Instanzen', 'href' => '/module/pihole/instances', 'active' => true],
+ ['label' => 'Listen', 'href' => '/module/pihole/lists'],
+ ['label' => 'Queries', 'href' => '/module/pihole/queries'],
+ ],
+]) ?>
-
Pi-hole
-
+
-
Pi-hole Instanzen hinzufuegen, bearbeiten und loeschen.
@@ -222,3 +230,4 @@ if ($primaryId === '') {
+= module_shell_footer() ?>
diff --git a/public/assets/css/app.css b/public/assets/css/app.css
index fdcc639..b9cca60 100644
--- a/public/assets/css/app.css
+++ b/public/assets/css/app.css
@@ -897,3 +897,192 @@ a {
.ip-dot.is-used {
background: color-mix(in srgb, var(--muted) 55%, var(--surface));
}
+
+.module-shell {
+ color: var(--text);
+}
+
+.module-page-bg {
+ position: relative;
+ padding: 8px 0 30px;
+}
+
+.module-page-bg::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background:
+ radial-gradient(circle at 12% 20%, color-mix(in srgb, var(--brand-accent-2) 12%, transparent), transparent 24%),
+ radial-gradient(circle at 90% 6%, color-mix(in srgb, var(--brand-accent-3) 12%, transparent), transparent 20%);
+}
+
+.module-page-stack {
+ position: relative;
+ display: grid;
+ gap: 18px;
+}
+
+.module-hero {
+ display: grid;
+ gap: 18px;
+ padding: 28px;
+ border: 1px solid var(--line);
+ border-radius: 28px;
+ background:
+ linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(245, 252, 251, 0.88)),
+ linear-gradient(90deg, color-mix(in srgb, var(--brand-accent) 14%, transparent), color-mix(in srgb, var(--brand-accent-2) 14%, transparent));
+ box-shadow: var(--shadow);
+}
+
+:root[data-theme="night"] .module-hero {
+ background:
+ linear-gradient(135deg, rgba(8, 18, 28, 0.94), rgba(15, 29, 42, 0.86)),
+ linear-gradient(90deg, color-mix(in srgb, var(--brand-accent) 18%, transparent), color-mix(in srgb, var(--brand-accent-2) 16%, transparent));
+}
+
+.module-hero-top {
+ display: grid;
+ gap: 16px;
+ grid-template-columns: minmax(0, 1.6fr) minmax(220px, 0.8fr);
+ align-items: start;
+}
+
+.module-hero-copy,
+.module-hero-actions {
+ display: grid;
+ gap: 12px;
+}
+
+.module-title {
+ margin: 0;
+ font-size: clamp(1.75rem, 4vw, 2.9rem);
+ line-height: 1;
+ font-weight: 700;
+ letter-spacing: -0.03em;
+}
+
+.module-lead {
+ margin: 0;
+ color: var(--muted);
+ font-size: 1rem;
+ line-height: 1.5;
+}
+
+.module-tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.module-button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid transparent;
+ border-radius: 16px;
+ padding: 12px 16px;
+ cursor: pointer;
+ text-decoration: none;
+ transition: 160ms ease;
+ font: inherit;
+}
+
+.module-button:hover {
+ transform: translateY(-1px);
+}
+
+.module-button--tab-active,
+.module-button--primary {
+ background: linear-gradient(135deg, var(--brand-accent), var(--brand-accent-3));
+ color: #fff7fb;
+ font-weight: 700;
+ box-shadow: 0 14px 28px color-mix(in srgb, var(--brand-accent) 18%, transparent);
+}
+
+.module-button--tab,
+.module-button--secondary {
+ background: rgba(255, 255, 255, 0.92);
+ color: #09111f;
+ font-weight: 700;
+}
+
+.module-button--ghost {
+ background: color-mix(in srgb, var(--brand-accent) 14%, transparent);
+ border-color: color-mix(in srgb, var(--brand-accent) 34%, transparent);
+ color: var(--brand-accent);
+ font-weight: 700;
+}
+
+.module-box,
+.module-box-soft,
+.module-box-table,
+.module-box-empty {
+ border: 1px solid var(--line);
+ border-radius: 22px;
+ background: var(--surface);
+ box-shadow: 0 12px 30px rgba(1, 22, 32, 0.08);
+ backdrop-filter: blur(8px);
+}
+
+.module-box,
+.module-box-soft,
+.module-box-table {
+ padding: 18px 20px;
+}
+
+.module-box-soft {
+ background: linear-gradient(180deg, rgba(255,255,255,0.96), rgba(248,252,252,0.92));
+}
+
+.module-box-empty {
+ padding: 20px;
+}
+
+.module-box-grid {
+ display: grid;
+ gap: 16px;
+}
+
+.module-box-grid--stats {
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+}
+
+.module-box-grid--panels {
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+}
+
+.module-box-head {
+ display: flex;
+ align-items: end;
+ justify-content: space-between;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.module-box-head p {
+ margin: 6px 0 0;
+ color: var(--muted);
+}
+
+.module-box-title {
+ margin: 0;
+ font-size: 1.1rem;
+ font-weight: 700;
+}
+
+.module-box-table {
+ overflow: auto;
+ padding: 0;
+}
+
+.module-box-table > .module-box-head,
+.module-box-table > .module-box-copy {
+ padding: 18px 20px 0;
+}
+
+@media (max-width: 980px) {
+ .module-hero-top {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/src/App/functions.php b/src/App/functions.php
index 5c74ed0..147c2c4 100644
--- a/src/App/functions.php
+++ b/src/App/functions.php
@@ -253,6 +253,96 @@ function module_tpl(string $module, string $name, array $data = []): void
}
}
+function module_design(string $module): array
+{
+ if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) {
+ return [];
+ }
+
+ static $cache = [];
+ if (array_key_exists($module, $cache)) {
+ return $cache[$module];
+ }
+
+ $path = __DIR__ . '/../../modules/' . $module . '/design.json';
+ if (!is_file($path)) {
+ $cache[$module] = [];
+ return $cache[$module];
+ }
+
+ $raw = file_get_contents($path);
+ $decoded = is_string($raw) && $raw !== '' ? json_decode($raw, true) : null;
+ $cache[$module] = is_array($decoded) ? $decoded : [];
+ return $cache[$module];
+}
+
+function module_shell_header(string $module, array $options = []): string
+{
+ $design = module_design($module);
+ $requestPath = app()->request()->path();
+ $title = trim((string) ($options['title'] ?? $design['title'] ?? ucfirst($module)));
+ $description = trim((string) ($options['description'] ?? $design['description'] ?? ''));
+ $eyebrow = trim((string) ($options['eyebrow'] ?? $design['eyebrow'] ?? 'Modul'));
+ $actions = is_array($options['actions'] ?? null) ? $options['actions'] : (is_array($design['actions'] ?? null) ? $design['actions'] : []);
+ $tabs = is_array($options['tabs'] ?? null) ? $options['tabs'] : (is_array($design['tabs'] ?? null) ? $design['tabs'] : []);
+
+ $html = '
';
+ $html .= '
';
+
+ return $html;
+}
+
+function module_shell_footer(): string
+{
+ return '
';
+}
+
/**
* HTML Escaping Helper.
*/