27 $name = strtolower(trim($name));
28 $name = preg_replace(
'/[^a-z0-9_-]+/',
'-', $name);
29 return trim((
string)$name,
'-_');
36 return preg_match(
'/^[a-z0-9][a-z0-9_-]{1,62}$/', $name) === 1;
45 throw new \InvalidArgumentException(
'Ungueltiger Designname.');
47 return $this->designRoot() . DIRECTORY_SEPARATOR . $name;
55 foreach (glob($this->
designRoot() . DIRECTORY_SEPARATOR .
'*', GLOB_ONLYDIR) ?: array() as
$dir) {
56 $name = basename(
$dir);
57 if (!$this->
isValidName($name) || !is_file(
$dir . DIRECTORY_SEPARATOR .
'htm' . DIRECTORY_SEPARATOR .
'default.htm')) {
64 'title' => trim((
string)($meta[
'title'] ??
'')) ?: ($name ===
'dbxapp' ?
'dbXapp' : ucfirst(str_replace(array(
'-',
'_'),
' ', $name))),
65 'description' => (
string)($meta[
'description'] ??
''),
66 'source_design' => (
string)($meta[
'source_design'] ??
''),
67 'layout' => (
string)($meta[
'layout'] ?? $this->detectLayout(
$dir)),
68 'managed' => is_file(
$dir . DIRECTORY_SEPARATOR .
'design.json'),
73 uasort(
$out,
static function(array $a, array $b):
int {
74 return strnatcasecmp($a[
'title'], $b[
'title']);
83 $file = $this->
designDir($name) . DIRECTORY_SEPARATOR .
'design.json';
84 if (!is_file(
$file)) {
87 $data = json_decode((
string)file_get_contents(
$file),
true);
88 return is_array($data) ? $data : array();
97 'target_design' =>
'',
101 'menu_style' =>
'tabs',
102 'content_width' =>
'wide',
103 'footer_mode' =>
'full',
104 'brand_name' =>
'Meine Website',
105 'tagline' =>
'Einfach. Klar. Persoenlich.',
106 'logo_mode' =>
'icon',
107 'logo_icon' =>
'bi-stars',
108 'primary_color' =>
'#2563eb',
109 'secondary_color' =>
'#0f172a',
110 'accent_color' =>
'#14b8a6',
111 'background_color' =>
'#f4f7fb',
112 'surface_color' =>
'#ffffff',
113 'text_color' =>
'#172033',
114 'font_family' =>
'modern',
116 'footer_text' =>
'Mit dbXapp erstellt',
134 throw new \InvalidArgumentException(
'Das Ausgangsdesign existiert nicht.');
136 if (!$this->isValidName(
$target)) {
137 throw new \InvalidArgumentException(
'Der technische Name muss 2 bis 63 Zeichen aus a-z, 0-9, - oder _ enthalten.');
140 throw new \InvalidArgumentException(
'Die Personalisierung wird als eigenes Design angelegt. Bitte einen neuen Namen verwenden.');
142 $targetDir = $this->designDir(
$target);
143 if (is_dir($targetDir)) {
144 throw new \RuntimeException(
'Das Zieldesign existiert bereits.');
147 $stage = $this->stageDir(
$target);
148 $this->removeDirectory($stage);
149 if (!mkdir($stage, 0777,
true) && !is_dir($stage)) {
150 throw new \RuntimeException(
'Staging-Verzeichnis konnte nicht angelegt werden.');
154 $this->copyDirectory($this->designDir(
$source), $stage);
156 $this->writeWizardFiles($stage,
$target,
$input, $logoUpload);
157 $validation = $this->validateDesignDirectory($stage,
true);
159 throw new \RuntimeException(implode(
' ',
$validation[
'errors']));
161 if (!rename($stage, $targetDir)) {
162 throw new \RuntimeException(
'Das fertige Design konnte nicht aktiviert werden.');
164 }
catch (\Throwable $e) {
165 $this->removeDirectory($stage);
169 if (!empty(
$input[
'set_default'])) {
182 'default_changed' => !empty(
$input[
'set_default']),
192 throw new \RuntimeException(
'Design fuer Backup nicht gefunden.');
194 if (!class_exists(
'ZipArchive')) {
195 throw new \RuntimeException(
'ZipArchive ist nicht verfuegbar.');
199 throw new \RuntimeException(
'Backup-Verzeichnis konnte nicht angelegt werden.');
201 $file =
$backupDir . DIRECTORY_SEPARATOR . date(
'Ymd-His') .
'-' . $name .
'.zip';
202 $zip = new \ZipArchive();
203 if ($zip->open(
$file, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !==
true) {
204 throw new \RuntimeException(
'Design-Backup konnte nicht erstellt werden.');
206 $this->addDirectoryToZip($zip,
$dir,
'');
217 $default =
$dir . DIRECTORY_SEPARATOR .
'htm' . DIRECTORY_SEPARATOR .
'default.htm';
218 if (!is_file($default)) {
219 $errors[] =
'htm/default.htm fehlt.';
220 return array(
'errors' =>
$errors,
'warnings' => $warnings);
222 $html = (string)file_get_contents($default);
223 if (substr_count(
$html,
'[dbx:content]') !== 1) {
224 $errors[] =
'htm/default.htm muss [dbx:content] genau einmal enthalten.';
226 foreach (array(
'{dbx:title}',
'{dbx:design}',
'{dbx:skin_css}',
'{dbx:skin_class}',
'dbx/js/lib/core.js') as
$required) {
231 $warnings[] =
'Template-Marker fehlt: ' .
$required;
235 foreach (array(
'logo',
'branding',
'footer') as $slot) {
236 $marker =
'[dbx:' . $slot .
']';
237 $fragmentFile =
$dir . DIRECTORY_SEPARATOR .
'htm' . DIRECTORY_SEPARATOR . $slot .
'.htm';
238 if (substr_count(
$html, $marker) > 1) {
239 $errors[] = $marker .
' darf in der Designschale hoechstens einmal vorkommen.';
241 if (strpos(
$html, $marker) !==
false && !is_file($fragmentFile)) {
242 $errors[] =
'Fragment fuer ' . $marker .
' fehlt.';
244 if (is_file($fragmentFile)) {
245 $fragmentContent = (string)file_get_contents($fragmentFile);
246 if (strpos($fragmentContent,
'[dbx:content]') !==
false) {
247 $errors[] =
'Fragment ' . $slot .
'.htm darf keinen Content-Slot enthalten.';
249 foreach (array(
'[dbx:logo]',
'[dbx:branding]',
'[dbx:footer]') as $nestedSlot) {
250 if (strpos($fragmentContent, $nestedSlot) !==
false) {
251 $errors[] =
'Fragment ' . $slot .
'.htm darf keine verschachtelten Design-Slots enthalten.';
257 foreach ($this->relativeFiles(
$dir) as $relative => $absolute) {
258 if (!$this->isAllowedDesignFile($relative)) {
259 $errors[] =
'Nicht erlaubte Designdatei: ' . $relative;
261 if (is_link($absolute)) {
262 $errors[] =
'Symbolische Links sind in Designpaketen nicht erlaubt: ' . $relative;
265 return array(
'errors' => array_values(array_unique(
$errors)),
'warnings' => array_values(array_unique($warnings)));
276 $root = rtrim(str_replace(
'\\',
'/',
$dir),
'/');
277 $it = new \RecursiveIteratorIterator(
278 new \RecursiveDirectoryIterator(
$dir, \FilesystemIterator::SKIP_DOTS)
284 $absolute =
$file->getPathname();
285 $relative = ltrim(substr(str_replace(
'\\',
'/', $absolute), strlen(
$root)),
'/');
286 $files[$relative] = $absolute;
296 $relative = str_replace(
'\\',
'/', $relative);
297 if ($relative ===
'' || $relative[0] ===
'/' || strpos($relative,
'..') !==
false || strpos($relative,
"\0") !==
false) {
300 $ext = strtolower(pathinfo($relative, PATHINFO_EXTENSION));
301 return in_array($ext, array(
302 'htm',
'html',
'css',
'js',
'json',
'txt',
'md',
303 'png',
'jpg',
'jpeg',
'webp',
'gif',
'svg',
'ico',
304 'woff',
'woff2',
'ttf',
'otf',
321 return 'Nicht erlaubte Ergebnisdatei: ' . $relative;
323 $ext = strtolower(pathinfo($relative, PATHINFO_EXTENSION));
324 if (!in_array($ext, array(
'htm',
'html',
'css',
'js',
'json',
'svg'),
true)) {
327 $content = (string)file_get_contents($absolute);
328 if (strpos(
$content,
'<?') !==
false) {
329 return 'Servercode ist in Designdateien nicht erlaubt: ' . $relative;
331 if ($ext ===
'json' && json_decode(
$content,
true) ===
null && trim(
$content) !==
'null') {
332 return 'Ungueltiges JSON im Design-Ergebnis: ' . $relative;
334 if ($ext ===
'htm' || $ext ===
'html') {
335 if (preg_match(
'/\son[a-z]+\s*=/i',
$content)
336 || preg_match(
'/javascript\s*:/i',
$content)
337 || preg_match(
'/<script\b(?![^>]*\bsrc\s*=)[^>]*>/i',
$content)
338 || preg_match(
'/<(?:script|link)\b[^>]*(?:src|href)\s*=\s*["\']?\s*(?:https?:)?\/\//i',
$content)) {
339 return 'Aktiver Inline- oder Fremdcode ist im Design-HTML nicht erlaubt: ' . $relative;
342 if ($ext ===
'css' && preg_match(
'/(?:@import|url\s*\()\s*["\']?\s*(?:https?:)?\/\//i',
$content)) {
343 return 'Externe CSS-Ressourcen sind im eigenstaendigen Design nicht erlaubt: ' . $relative;
345 if ($ext ===
'js' && preg_match(
'/\b(?:fetch|XMLHttpRequest|WebSocket|EventSource|sendBeacon)\s*(?:\(|\.)/i',
$content)) {
346 return 'Eigener Netzwerktransport ist im Design-JavaScript nicht erlaubt: ' . $relative;
348 if ($ext ===
'svg' && preg_match(
'/<(?:script|foreignObject)\b|\son[a-z]+\s*=|javascript\s*:|<!ENTITY|<\?xml-stylesheet/i',
$content)) {
349 return 'Aktiver SVG-Inhalt ist im Design-Ergebnis nicht erlaubt: ' . $relative;
361 $mode =
$mode ===
'update' ?
'update' :
'create';
363 throw new \InvalidArgumentException(
'Ungueltiges Zieldesign.');
365 $targetDir = $this->designDir(
$target);
367 if (
$mode ===
'update') {
368 if (!is_dir($targetDir)) {
369 throw new \RuntimeException(
'Das zu aktualisierende Design existiert nicht.');
372 } elseif (is_dir($targetDir)) {
373 throw new \RuntimeException(
'Das Zieldesign existiert bereits.');
376 $stage = $this->stageDir(
$target);
377 $this->removeDirectory($stage);
378 if (!mkdir($stage, 0777,
true) && !is_dir($stage)) {
379 throw new \RuntimeException(
'Staging-Verzeichnis konnte nicht angelegt werden.');
383 if (
$mode ===
'update') {
384 $this->copyDirectory($targetDir, $stage);
386 if (!$this->isValidName(
$source) || !is_dir($this->designDir(
$source))) {
387 throw new \RuntimeException(
'Ausgangsdesign fuer die Neuanlage fehlt.');
389 $this->copyDirectory($this->designDir(
$source), $stage);
392 foreach ($this->relativeFiles($resultDir) as $relative => $absolute) {
393 $fileError = $this->validateResultFile($relative, $absolute);
394 if ($fileError !==
'') {
395 throw new \RuntimeException($fileError);
397 $dest = $stage . DIRECTORY_SEPARATOR . str_replace(
'/', DIRECTORY_SEPARATOR, $relative);
398 $parent = dirname($dest);
399 if (!is_dir($parent) && !mkdir($parent, 0777,
true) && !is_dir($parent)) {
400 throw new \RuntimeException(
'Zielordner konnte nicht angelegt werden: ' . $relative);
402 if (!copy($absolute, $dest)) {
403 throw new \RuntimeException(
'Ergebnisdatei konnte nicht uebernommen werden: ' . $relative);
406 $validation = $this->validateDesignDirectory($stage,
true);
408 throw new \RuntimeException(implode(
' ',
$validation[
'errors']));
410 if (
$mode ===
'update') {
411 $old = $targetDir .
'.dbx-old-' . bin2hex(random_bytes(4));
412 if (!rename($targetDir, $old)) {
413 throw new \RuntimeException(
'Bestehendes Design konnte nicht fuer den Austausch vorbereitet werden.');
415 if (!rename($stage, $targetDir)) {
416 rename($old, $targetDir);
417 throw new \RuntimeException(
'Neues Design konnte nicht aktiviert werden.');
419 $this->removeDirectory($old);
420 } elseif (!rename($stage, $targetDir)) {
421 throw new \RuntimeException(
'Neues Design konnte nicht aktiviert werden.');
423 }
catch (\Throwable $e) {
424 $this->removeDirectory($stage);
431 private function stageDir(
string $target): string {
434 throw new \RuntimeException(
'Design-Staging-Root konnte nicht angelegt werden.');
436 return $root . DIRECTORY_SEPARATOR .
$target .
'-' . bin2hex(random_bytes(5));
439 private function detectLayout(
string $dir): string {
440 $html = is_file(
$dir .
'/htm/default.htm') ? (string)file_get_contents(
$dir .
'/htm/default.htm') :
'';
441 return strpos(
$html,
'<aside') !==
false ?
'sidebar' :
'top';
444 private function copyDirectory(
string $source,
string $target): void {
445 $it = new \RecursiveIteratorIterator(
446 new \RecursiveDirectoryIterator(
$source, \FilesystemIterator::SKIP_DOTS),
447 \RecursiveIteratorIterator::SELF_FIRST
450 if (
$file->isLink()) {
453 $relative = substr(
$file->getPathname(), strlen(rtrim(
$source,
'/\\')) + 1);
454 $dest =
$target . DIRECTORY_SEPARATOR . $relative;
455 if (
$file->isDir()) {
456 if (!is_dir($dest) && !mkdir($dest, 0777,
true) && !is_dir($dest)) {
457 throw new \RuntimeException(
'Ordner konnte nicht kopiert werden: ' . $relative);
459 } elseif (!copy(
$file->getPathname(), $dest)) {
460 throw new \RuntimeException(
'Datei konnte nicht kopiert werden: ' . $relative);
465 private function removeDirectory(
string $dir): void {
469 $it = new \RecursiveIteratorIterator(
470 new \RecursiveDirectoryIterator(
$dir, \FilesystemIterator::SKIP_DOTS),
471 \RecursiveIteratorIterator::CHILD_FIRST
474 if (
$file->isDir()) {
475 @rmdir(
$file->getPathname());
477 @unlink(
$file->getPathname());
483 private function rewriteDesignPaths(
string $dir,
string $source,
string $target): void {
484 foreach ($this->relativeFiles(
$dir) as $relative => $absolute) {
485 $ext = strtolower(pathinfo($relative, PATHINFO_EXTENSION));
486 if (!in_array($ext, array(
'htm',
'html',
'css',
'js',
'json',
'txt',
'md'),
true)) {
489 $content = (string)file_get_contents($absolute);
496 file_put_contents($absolute, $new);
501 private function writeWizardFiles(
string $dir,
string $target, array
$input, array $logoUpload): void {
502 foreach (array(
'htm',
'css',
'img',
'js') as $sub) {
503 if (!is_dir(
$dir . DIRECTORY_SEPARATOR . $sub)) {
504 mkdir(
$dir . DIRECTORY_SEPARATOR . $sub, 0777,
true);
507 $input = array_merge($this->defaults((
string)(
$input[
'source_design'] ??
'dbxapp')),
$input);
508 $logoAsset = $this->storeLogoUpload(
$dir, $logoUpload);
509 file_put_contents(
$dir .
'/htm/default.htm', $this->buildDefaultTemplate(
$target,
$input));
510 file_put_contents(
$dir .
'/htm/logo.htm', $this->buildLogoFragment(
$input, $logoAsset));
511 file_put_contents(
$dir .
'/htm/branding.htm', $this->buildBrandingFragment(
$input));
512 file_put_contents(
$dir .
'/htm/footer.htm', $this->buildFooterFragment(
$input));
513 file_put_contents(
$dir .
'/css/design-custom.css', $this->buildCustomCss(
$input));
515 'contract' => self::CONTRACT,
517 'created_at' => date(DATE_ATOM),
518 'managed_by' =>
'dbxDesign_admin',
519 'slots' => array(
'logo',
'branding',
'footer'),
520 'logo_asset' => $logoAsset,
524 $dir .
'/design.json',
525 json_encode(
$metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
529 private function storeLogoUpload(
string $dir, array
$file): string {
530 if (!
$file || (
$file[
'error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
533 if ((
$file[
'error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
534 throw new \InvalidArgumentException(
'Logo-Upload fehlgeschlagen.');
536 if ((
int)(
$file[
'size'] ?? 0) > 5 * 1024 * 1024) {
537 throw new \InvalidArgumentException(
'Das Logo darf maximal 5 MB gross sein.');
539 $tmp = (string)(
$file[
'tmp_name'] ??
'');
540 $mime =
$tmp !==
'' && is_file(
$tmp) && function_exists(
'mime_content_type') ? (string)mime_content_type(
$tmp) :
'';
542 'image/png' =>
'png',
543 'image/jpeg' =>
'jpg',
544 'image/webp' =>
'webp',
545 'image/gif' =>
'gif',
548 throw new \InvalidArgumentException(
'Logo bitte als PNG, JPG, WEBP oder GIF hochladen.');
551 $dest =
$dir . DIRECTORY_SEPARATOR .
'img' . DIRECTORY_SEPARATOR . $name;
552 if (!copy(
$tmp, $dest)) {
553 throw new \RuntimeException(
'Logo konnte nicht gespeichert werden.');
555 return 'img/' . $name;
558 private function buildDefaultTemplate(
string $target, array $in): string {
559 $layout = in_array($in[
'layout'], array(
'top',
'sidebar',
'hybrid'), true) ? $in[
'layout'] :
'top';
560 $extraCss = is_file($this->designDir((
string)$in[
'source_design']) .
'/css/glass-3d.css')
561 ?
' <link rel="stylesheet" href="dbx/design/{dbx:design}/css/glass-3d.css">' .
"\n"
563 $adminMenu = <<<
'HTML'
565<nav
id=
"dbx_admin_menu" class=
"dbx-menu dbx-menu-admin" data-
dbx=
'lib=menu|id=admin|label=<i class="bi bi-shield-lock"></i> Admin' style=
"visibility:hidden">
566 [modul=
dbxMenu]dbx_run1=load&tpl=modul|
dbx-top-admin[/modul]
570 $mainMenu = <<<
'HTML'
571<nav
id=
"dbx_main_menu" class=
"dbx-menu dbx-menu-main" data-
dbx=
'lib=menu|id=user|label=<i class="bi bi-list"></i> Menue' style=
"visibility:hidden">
572 [modul=
dbxMenu]dbx_run1=load&tpl=modul|
dbx-top-main[/modul]
576 $shell =
'<div id="dbxApp" class="dbx-design-shell dbx-layout-sidebar">'
577 .
'<aside id="dbxHeader" class="dbx-design-side"><div class="dbx-brand-row">[dbx:logo][dbx:branding]</div>'
578 . $mainMenu .
'<div class="dbx-admin-side">' . $adminMenu .
'</div></aside>'
579 .
'<div class="dbx-design-stage"><main id="dbxMain"><div class="dbx-content"><div id="dbxContent" class="dbx-content-inner">[dbx:content]</div></div></main>[dbx:footer]</div></div>';
580 } elseif (
$layout ===
'hybrid') {
581 $shell =
'<div id="dbxApp" class="dbx-design-shell dbx-layout-hybrid">'
582 .
'<header id="dbxHeader"><div class="dbx-brand-row">[dbx:logo][dbx:branding]</div>' . $adminMenu .
'</header>'
583 .
'<div class="dbx-hybrid-grid"><aside class="dbx-design-side">' . $mainMenu .
'</aside>'
584 .
'<div class="dbx-design-stage"><main id="dbxMain"><div class="dbx-content"><div id="dbxContent" class="dbx-content-inner">[dbx:content]</div></div></main>[dbx:footer]</div></div></div>';
586 $shell =
'<div id="dbxApp" class="dbx-design-shell dbx-layout-top">'
587 .
'<header id="dbxHeader"><div class="dbx-brand-row">[dbx:logo][dbx:branding]</div><div class="dbx-menu-stack">'
588 . $adminMenu . $mainMenu .
'</div></header>'
589 .
'<main id="dbxMain"><div class="dbx-content"><div id="dbxContent" class="dbx-content-inner">[dbx:content]</div></div></main>[dbx:footer]</div>';
591 return '<!DOCTYPE html>' .
"\n"
592 .
'<html lang="{dbx:lng}"><head>' .
"\n"
593 .
' <meta charset="utf-8">' .
"\n"
594 .
' <meta name="viewport" content="width=device-width, initial-scale=1">' .
"\n"
595 .
' <title>{dbx:title}</title>{dbx:head_meta}' .
"\n"
596 .
' <link rel="stylesheet" href="dbx/vendor/twbs/bootstrap/dist/css/bootstrap.min.css">' .
"\n"
597 .
' <link rel="stylesheet" href="dbx/vendor/twbs/bootstrap-icons/font/bootstrap-icons.css">' .
"\n"
598 .
' <link rel="stylesheet" href="dbx/design/{dbx:design}/css/colors.css">' .
"\n"
599 .
' <link rel="stylesheet" href="{dbx:skin_css}">' .
"\n"
600 .
' <link rel="stylesheet" href="dbx/design/{dbx:design}/css/base.css">' .
"\n"
601 .
' <link rel="stylesheet" href="dbx/design/{dbx:design}/css/theme.css">' .
"\n"
603 .
' <link rel="stylesheet" href="dbx/design/{dbx:design}/css/design-custom.css">' .
"\n"
605 .
'<body class="dbx-app dbx-design-generated dbx-layout-' .
$layout .
' {dbx:skin_class}"'
606 .
'[inc=is_dbx_edit()] data-dbx="lib=icons_editor"[/inc] data-dbx-design="{dbx:design}" data-dbx-skin="{dbx:color}">' .
"\n"
608 .
'<div id="dbxPreloader" class="dbx-preloader" style="display:none!important;pointer-events:none!important"></div>' .
"\n"
609 .
'<button id="dbxBackToTop" class="btn btn-primary" type="button" aria-label="Nach oben"><i class="bi bi-arrow-up"></i></button>' .
"\n"
610 .
'<script src="dbx/vendor/components/jquery/jquery.min.js"></script>' .
"\n"
611 .
'<script src="dbx/js/lib/core.js?design={dbx:design}"></script>' .
"\n"
615 private function buildLogoFragment(array $in,
string $asset): string {
616 $name = htmlspecialchars((string)$in[
'brand_name'], ENT_QUOTES,
'UTF-8');
618 return '<a class="dbx-design-logo" href="{base_url}" aria-label="' . $name .
'"><img src="dbx/design/{dbx:design}/' . $asset .
'" alt="' . $name .
'"></a>';
620 if (($in[
'logo_mode'] ??
'icon') ===
'none') {
623 $icon = preg_replace(
'/[^a-z0-9_-]+/i',
'', (
string)($in[
'logo_icon'] ??
'bi-stars')) ?:
'bi-stars';
624 return '<a class="dbx-design-logo dbx-design-logo-icon" href="{base_url}" aria-label="' . $name .
'"><i class="bi ' . $icon .
'" aria-hidden="true"></i></a>';
627 private function buildBrandingFragment(array $in): string {
628 $name = htmlspecialchars((string)$in[
'brand_name'], ENT_QUOTES,
'UTF-8');
629 $tagline = htmlspecialchars((
string)$in[
'tagline'], ENT_QUOTES,
'UTF-8');
630 return '<a class="dbx-design-branding" href="{base_url}"><strong>' . $name .
'</strong>'
631 . ($tagline !==
'' ?
'<small>' . $tagline .
'</small>' :
'') .
'</a>';
634 private function buildFooterFragment(array $in): string {
635 $mode = in_array($in[
'footer_mode'], array(
'full',
'minimal',
'none'), true) ? $in[
'footer_mode'] :
'full';
636 $dock =
'<div class="dbx-footer-dockbar" aria-label="Fensterleiste"><div id="windrop" class="dbx-window-dock" aria-label="Minimierte Fenster"></div>'
637 .
'<button id="dbxWindowCloseAll" class="dbx-footer-window-close-all" type="button" title="Alle Fenster schliessen" aria-label="Alle Fenster schliessen"><i class="bi bi-x-square"></i></button></div>';
638 if (
$mode ===
'none') {
639 return '<footer id="dbxFooter" class="dbx-design-footer dbx-footer-dock-only">' . $dock .
'</footer>';
641 $brand = htmlspecialchars((
string)$in[
'brand_name'], ENT_QUOTES,
'UTF-8');
642 $text = htmlspecialchars((
string)$in[
'footer_text'], ENT_QUOTES,
'UTF-8');
643 $legal = !empty($in[
'legal_links'])
644 ?
'<a href="datenschutz">Datenschutz</a><a href="impressum">Impressum</a>'
646 $details =
$mode ===
'full' && $text !==
'' ?
'<span class="dbx-design-footer-text">' . $text .
'</span>' :
'';
647 return '<footer id="dbxFooter" class="dbx-design-footer">' . $dock
648 .
'<div class="dbx-design-footer-info">' . $details .
'<span class="dbx-design-footer-links">' . $legal .
'</span>'
649 .
'<span>© ' . date(
'Y') .
' ' . $brand .
'</span><span data-dbx-runtime data-dbx-php-runtime class="visually-hidden">...</span></div></footer>';
652 private function buildCustomCss(array $in): string {
654 'primary_color' =>
'#2563eb',
655 'secondary_color' =>
'#0f172a',
656 'accent_color' =>
'#14b8a6',
657 'background_color' =>
'#f4f7fb',
658 'surface_color' =>
'#ffffff',
659 'text_color' =>
'#172033',
662 if (preg_match(
'/^#[0-9a-f]{6}$/i', (
string)($in[$key] ??
'')) !== 1) {
667 'system' =>
'system-ui, -apple-system, "Segoe UI", sans-serif',
668 'modern' =>
'"Inter", "Segoe UI", system-ui, sans-serif',
669 'editorial' =>
'Georgia, "Times New Roman", serif',
670 'rounded' =>
'"Trebuchet MS", "Segoe UI", sans-serif',
672 $radius = array(
'square' =>
'0.25rem',
'soft' =>
'0.85rem',
'round' =>
'1.5rem');
673 $width = array(
'compact' =>
'960px',
'wide' =>
'1440px',
'full' =>
'100%');
674 $font = $fonts[$in[
'font_family']] ?? $fonts[
'modern'];
675 $radiusValue = $radius[$in[
'radius']] ?? $radius[
'soft'];
676 $maxWidth = $width[$in[
'content_width']] ?? $width[
'wide'];
677 $menuStyle = in_array($in[
'menu_style'], array(
'tabs',
'pills',
'compact'),
true) ? $in[
'menu_style'] :
'tabs';
678 return '/* Durch dbxDesign_admin erzeugte, bewusst kleine Anpassungsschicht. */' .
"\n"
680 .
' --design-primary:' . $in[
'primary_color'] .
';--design-secondary:' . $in[
'secondary_color'] .
';--design-accent:' . $in[
'accent_color'] .
';' .
"\n"
681 .
' --design-bg:' . $in[
'background_color'] .
';--design-surface:' . $in[
'surface_color'] .
';--design-text:' . $in[
'text_color'] .
';' .
"\n"
682 .
' --design-radius:' . $radiusValue .
';--design-content-width:' . $maxWidth .
';' .
"\n"
683 .
' --dbx-primary:var(--design-primary);--bs-primary:var(--design-primary);' .
"\n"
685 .
'html,body{min-height:100%;}body.dbx-design-generated{margin:0;background:var(--design-bg);color:var(--design-text);font-family:' . $font .
';}' .
"\n"
686 .
'.dbx-design-shell{min-height:100vh;}.dbx-design-stage{min-width:0;display:flex;flex-direction:column;min-height:100vh;}' .
"\n"
687 .
'#dbxHeader{background:var(--design-surface);border-bottom:1px solid color-mix(in srgb,var(--design-secondary) 18%,transparent);box-shadow:0 8px 30px color-mix(in srgb,var(--design-secondary) 10%,transparent);}' .
"\n"
688 .
'.dbx-brand-row{display:flex;align-items:center;gap:.8rem;padding:.8rem 1rem;}.dbx-design-logo{display:grid;place-items:center;width:2.8rem;height:2.8rem;border-radius:var(--design-radius);background:var(--design-primary);color:#fff;font-size:1.35rem;overflow:hidden;flex:0 0 auto;}.dbx-design-logo img{width:100%;height:100%;object-fit:contain;background:#fff;}.dbx-design-branding{display:grid;color:var(--design-text);text-decoration:none;line-height:1.15;}.dbx-design-branding strong{font-size:1.05rem}.dbx-design-branding small{margin-top:.2rem;color:color-mix(in srgb,var(--design-text) 68%,transparent);}' .
"\n"
689 .
'.dbx-menu-stack{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;padding:0 1rem .8rem}.dbx-layout-top #dbxHeader{display:flex;align-items:center;justify-content:space-between;gap:1rem;position:sticky;top:0;z-index:1030}.dbx-layout-top .dbx-menu-stack{padding:.7rem 1rem;}' .
"\n"
690 .
'.dbx-layout-sidebar{display:grid;grid-template-columns:minmax(230px,290px) minmax(0,1fr)}.dbx-layout-sidebar>.dbx-design-side,.dbx-hybrid-grid>.dbx-design-side{min-height:100vh;border-right:1px solid color-mix(in srgb,var(--design-secondary) 16%,transparent);background:var(--design-surface);}.dbx-layout-sidebar>.dbx-design-side{position:sticky;top:0;height:100vh;overflow:auto}.dbx-layout-sidebar .dbx-menu,.dbx-hybrid-grid>.dbx-design-side .dbx-menu{display:block;padding:.5rem 1rem}.dbx-admin-side{margin-top:auto;padding-top:1rem}' .
"\n"
691 .
'.dbx-hybrid-grid{display:grid;grid-template-columns:minmax(210px,260px) minmax(0,1fr)}.dbx-layout-hybrid>#dbxHeader{display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:1030}.dbx-hybrid-grid>.dbx-design-side{min-height:calc(100vh - 70px);}' .
"\n"
692 .
'#dbxMain{flex:1;min-width:0}.dbx-content-inner{width:min(100%,var(--design-content-width));margin-inline:auto;padding:clamp(.75rem,2vw,1.5rem)}' .
"\n"
693 .
'.dbx-design-footer{margin-top:auto;background:var(--design-surface);border-top:1px solid color-mix(in srgb,var(--design-secondary) 16%,transparent)}.dbx-design-footer-info{display:flex;flex-wrap:wrap;justify-content:space-between;gap:.75rem;padding:.8rem 1rem;font-size:.85rem}.dbx-design-footer-links{display:flex;gap:1rem}.dbx-footer-dock-only .dbx-footer-dockbar:empty{display:none}' .
"\n"
694 .
'.dbx-design-generated .btn-primary{--bs-btn-bg:var(--design-primary);--bs-btn-border-color:var(--design-primary)}.dbx-design-generated .card,.dbx-design-generated .dbx-panel{border-radius:var(--design-radius)}' .
"\n"
695 . ($menuStyle ===
'pills' ?
'.dbx-design-generated .dbx-menu a{border-radius:999px}' : ($menuStyle ===
'compact' ?
'.dbx-design-generated .dbx-menu a{padding:.3rem .55rem;font-size:.88rem}' :
'.dbx-design-generated .dbx-menu a{border-radius:var(--design-radius)}')) .
"\n"
696 .
'@media(max-width:900px){.dbx-layout-top #dbxHeader{position:relative;display:block}.dbx-layout-sidebar,.dbx-hybrid-grid{grid-template-columns:1fr}.dbx-layout-sidebar>.dbx-design-side,.dbx-hybrid-grid>.dbx-design-side{position:relative;height:auto;min-height:0;border-right:0;border-bottom:1px solid color-mix(in srgb,var(--design-secondary) 16%,transparent)}.dbx-menu-stack{display:block}.dbx-design-stage{min-height:auto}}' .
"\n";
699 private function addDirectoryToZip(\ZipArchive $zip,
string $dir,
string $prefix): void {
700 foreach ($this->relativeFiles(
$dir) as $relative => $absolute) {
701 $zip->addFile($absolute, ltrim($prefix .
'/' . $relative,
'/'));
if(preg_match('/core\.js\? foreach[^"\']*v=(\d+)/', $designTemplate, $assetMatch) !== 1 || !str_contains($shopReference, 'dbxapp-Asset-Version ' . $assetMatch[1])) foreach (array( 'reference\\archive', 'provision_docs_content.php', 'dbxSelfTest') as $needle)(array('Installation'=> $installation, 'Installations-Tutorial'=> $installationTutorial, 'SelfTest'=> $selfTest) as $label=> $html)