UI Skins
All checks were successful
Deploy / deploy-staging (push) Successful in 11s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-08 00:30:18 +02:00
parent 538175e52e
commit a8b197592b
14 changed files with 476 additions and 57 deletions

View File

@@ -73,6 +73,7 @@ Offen:
- frei platzierbare Icons sind noch nicht umgesetzt - frei platzierbare Icons sind noch nicht umgesetzt
- Layout und App-Anordnung des Startmenues sind noch offen - Layout und App-Anordnung des Startmenues sind noch offen
- Skins muessen echte OS-Layouts 1:1 widerspiegeln: Farbschema, Leistenposition, Leistenverhalten, Fensterchrome und Desktop-Haptik fuer Windows, Apple und Linux Mint
- persoenliche Startseite und oeffentliches Home sind noch nicht getrennt funktional - persoenliche Startseite und oeffentliches Home sind noch nicht getrennt funktional
- eingebettete Webseiten, Links, externe Integrationen und Dashboard-Inhalte fehlen - eingebettete Webseiten, Links, externe Integrationen und Dashboard-Inhalte fehlen
@@ -93,12 +94,14 @@ Erfuellt:
- `src/Desktop/` ist als globaler Desktop-Core vorhanden - `src/Desktop/` ist als globaler Desktop-Core vorhanden
- `public/assets/desktop/` und `partials/desktop/` sind angelegt - `public/assets/desktop/` und `partials/desktop/` sind angelegt
- Trennung zwischen globalem System, Desktop-UI und Modulen ist strukturell vorbereitet - Trennung zwischen globalem System, Desktop-UI und Modulen ist strukturell vorbereitet
- Skin-System wird auf gemeinsame Basis plus Skin-spezifische Ordnerstruktur vorbereitet
Offen: Offen:
- `modules/<modul>/` im neuen Projekt enthalten noch keine wirklich angebundenen Module - `modules/<modul>/` im neuen Projekt enthalten noch keine wirklich angebundenen Module
- `partials/landingpages/` und `partials/structure/` sind nur minimal vorbereitet - `partials/landingpages/` und `partials/structure/` sind nur minimal vorbereitet
- `tools/` und `debug/` sind noch nicht mit Funktion befuellt - `tools/` und `debug/` sind noch nicht mit Funktion befuellt
- Skin-Ordnerkonzept mit `base`-Fallback sowie pro Skin getrennten Layout-/Asset-Dateien fachlich fertigziehen und abnehmen
Nachweise: Nachweise:
@@ -149,6 +152,7 @@ Phase 2 Desktop-Grundfunktionen:
- `TEILWEISE`: Statusbereich - `TEILWEISE`: Statusbereich
- `ERLEDIGT`: Uhr - `ERLEDIGT`: Uhr
- `ERLEDIGT`: erstes Hauptmenue oeffnet und schliesst per Menu-Button - `ERLEDIGT`: erstes Hauptmenue oeffnet und schliesst per Menu-Button
- `OFFEN`: echte OS-spezifische Desktop-Layouts fuer Windows, Apple und Linux Mint
Phase 3 Fenster-Manager: Phase 3 Fenster-Manager:
@@ -156,6 +160,7 @@ Phase 3 Fenster-Manager:
- `ERLEDIGT`: Fenster fokussieren - `ERLEDIGT`: Fenster fokussieren
- `ERLEDIGT`: Fenster minimieren - `ERLEDIGT`: Fenster minimieren
- `ERLEDIGT`: Fenster maximieren - `ERLEDIGT`: Fenster maximieren
- `ERLEDIGT`: Fenster-Layout und deckende Darstellung
- `ERLEDIGT`: z-index-Verwaltung - `ERLEDIGT`: z-index-Verwaltung
- `TEILWEISE`: Position und Groesse merken, technisch vorbereitet aber noch nicht abgenommen - `TEILWEISE`: Position und Groesse merken, technisch vorbereitet aber noch nicht abgenommen
@@ -258,6 +263,7 @@ Hauptdateien:
### Prioritaet A ### Prioritaet A
- echte OS-Skins mit eigenem Layout und Verhalten fuer Windows, Apple und Linux Mint fertigziehen
- echtes Login mit Keycloak anbinden - echtes Login mit Keycloak anbinden
- persoenlichen Desktop-Zustand pro Nutzer speichern - persoenlichen Desktop-Zustand pro Nutzer speichern
- Admin-App in echte Verwaltungsseiten zerlegen - Admin-App in echte Verwaltungsseiten zerlegen

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
$payloadJson = json_encode($desktopPayload, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); $payloadJson = json_encode($desktopPayload, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
$activeSkin = $desktopPayload['meta']['active_skin']; $activeSkin = $desktopPayload['meta']['active_skin'];
$baseSkinProfile = $desktopPayload['meta']['base_skin_profile'];
$skinProfile = $desktopPayload['meta']['skin_profile'];
$wallpaper = $desktopPayload['desktop']['wallpaper']; $wallpaper = $desktopPayload['desktop']['wallpaper'];
?><!DOCTYPE html> ?><!DOCTYPE html>
<html lang="de"> <html lang="de">
@@ -12,9 +14,37 @@ $wallpaper = $desktopPayload['desktop']['wallpaper'];
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($desktopPayload['meta']['title'], ENT_QUOTES) ?></title> <title><?= htmlspecialchars($desktopPayload['meta']['title'], ENT_QUOTES) ?></title>
<link rel="stylesheet" href="/assets/desktop/desktop.css"> <link rel="stylesheet" href="/assets/desktop/desktop.css">
<?php if (!empty($baseSkinProfile['css_path'])): ?>
<link rel="stylesheet" href="<?= htmlspecialchars((string) $baseSkinProfile['css_path'], ENT_QUOTES) ?>">
<?php endif; ?>
<?php if (!empty($skinProfile['css_path']) && $skinProfile['css_path'] !== $baseSkinProfile['css_path']): ?>
<link rel="stylesheet" href="<?= htmlspecialchars((string) $skinProfile['css_path'], ENT_QUOTES) ?>">
<?php endif; ?>
</head> </head>
<body data-skin="<?= htmlspecialchars($activeSkin, ENT_QUOTES) ?>" style="--wallpaper: <?= htmlspecialchars($wallpaper, ENT_QUOTES) ?>;"> <body
data-skin="<?= htmlspecialchars($activeSkin, ENT_QUOTES) ?>"
data-skin-family="<?= htmlspecialchars((string) ($skinProfile['family'] ?? $activeSkin), ENT_QUOTES) ?>"
data-taskbar-mode="<?= htmlspecialchars((string) ($skinProfile['taskbar_mode'] ?? 'taskbar'), ENT_QUOTES) ?>"
style="--wallpaper: <?= htmlspecialchars($wallpaper, ENT_QUOTES) ?>;"
>
<div class="desktop-shell"> <div class="desktop-shell">
<?php if (($skinProfile['top_bar'] ?? false) === true): ?>
<header class="system-bar">
<div class="system-bar-left">
<span class="system-brand"><?= htmlspecialchars((string) ($skinProfile['label'] ?? ''), ENT_QUOTES) ?></span>
<span class="system-divider"></span>
<span>Finder</span>
<span>Ablage</span>
<span>Bearbeiten</span>
<span>Fenster</span>
</div>
<div class="system-bar-right">
<div class="tray-skins system-skins" id="tray-skins-top"></div>
<span class="system-divider"></span>
<time id="clock-top"></time>
</div>
</header>
<?php endif; ?>
<main class="desktop-stage"> <main class="desktop-stage">
<div class="desktop-icons" id="desktop-icons"></div> <div class="desktop-icons" id="desktop-icons"></div>
@@ -61,7 +91,10 @@ $wallpaper = $desktopPayload['desktop']['wallpaper'];
</section> </section>
<footer class="taskbar"> <footer class="taskbar">
<button class="start-button" id="start-button" type="button">Menu</button> <button class="start-button" id="start-button" type="button">
<span class="start-button-icon"><?= htmlspecialchars((string) ($skinProfile['taskbar_icon'] ?? ''), ENT_QUOTES) ?></span>
<span class="start-button-label"><?= htmlspecialchars((string) ($skinProfile['taskbar_label'] ?? 'Menu'), ENT_QUOTES) ?></span>
</button>
<div class="taskbar-apps" id="taskbar-apps"></div> <div class="taskbar-apps" id="taskbar-apps"></div>
<div class="tray"> <div class="tray">
<?php foreach ($desktopPayload['tray'] as $trayItem): ?> <?php foreach ($desktopPayload['tray'] as $trayItem): ?>
@@ -76,5 +109,11 @@ $wallpaper = $desktopPayload['desktop']['wallpaper'];
<script id="desktop-payload" type="application/json"><?= $payloadJson ?></script> <script id="desktop-payload" type="application/json"><?= $payloadJson ?></script>
<script src="/assets/desktop/desktop.js"></script> <script src="/assets/desktop/desktop.js"></script>
<?php if (!empty($baseSkinProfile['js_path'])): ?>
<script src="<?= htmlspecialchars((string) $baseSkinProfile['js_path'], ENT_QUOTES) ?>"></script>
<?php endif; ?>
<?php if (!empty($skinProfile['js_path']) && $skinProfile['js_path'] !== $baseSkinProfile['js_path']): ?>
<script src="<?= htmlspecialchars((string) $skinProfile['js_path'], ENT_QUOTES) ?>"></script>
<?php endif; ?>
</body> </body>
</html> </html>

View File

@@ -1,7 +1,6 @@
:root { :root {
color-scheme: light; color-scheme: light;
--shell-text: #f8fafc; --shell-text: #f8fafc;
--shell-panel: rgba(15, 23, 42, 0.42);
--shell-border: rgba(255, 255, 255, 0.16); --shell-border: rgba(255, 255, 255, 0.16);
--card-bg: rgba(255, 255, 255, 0.14); --card-bg: rgba(255, 255, 255, 0.14);
--window-bg: #f8fafc; --window-bg: #f8fafc;
@@ -10,7 +9,18 @@
--accent: #60a5fa; --accent: #60a5fa;
--taskbar-bg: rgba(15, 23, 42, 0.8); --taskbar-bg: rgba(15, 23, 42, 0.8);
--shadow: 0 16px 40px rgba(15, 23, 42, 0.35); --shadow: 0 16px 40px rgba(15, 23, 42, 0.35);
font-family: "IBM Plex Sans", "Segoe UI", sans-serif; --desktop-font: "IBM Plex Sans", "Segoe UI", sans-serif;
--taskbar-height: 60px;
--taskbar-left: 24px;
--taskbar-right: 24px;
--taskbar-bottom: 24px;
--taskbar-width: auto;
--taskbar-radius: 22px;
--taskbar-justify: flex-start;
--stage-top-offset: 0px;
--icon-top: 24px;
--icon-left: 24px;
font-family: var(--desktop-font);
} }
* { * {
@@ -24,24 +34,44 @@ body {
color: var(--shell-text); color: var(--shell-text);
} }
body[data-skin="apple"] {
--accent: #fb7185;
--taskbar-bg: rgba(45, 45, 45, 0.65);
--window-bg: #fffbeb;
--window-header-bg: #fff1d6;
}
body[data-skin="linux"] {
--accent: #f97316;
--taskbar-bg: rgba(17, 24, 39, 0.86);
--window-bg: #f9fafb;
--window-header-bg: #eceff3;
}
.desktop-shell { .desktop-shell {
min-height: 100vh; min-height: 100vh;
} }
.system-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 140;
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
padding: 0 14px;
background: rgba(244, 244, 245, 0.82);
color: #111827;
backdrop-filter: blur(20px);
}
.system-bar-left,
.system-bar-right {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
}
.system-brand {
font-weight: 700;
}
.system-divider {
width: 1px;
height: 14px;
background: rgba(17, 24, 39, 0.2);
}
.eyebrow { .eyebrow {
margin: 0 0 12px; margin: 0 0 12px;
text-transform: uppercase; text-transform: uppercase;
@@ -79,8 +109,8 @@ h1 {
.desktop-icons { .desktop-icons {
position: absolute; position: absolute;
top: 24px; top: var(--icon-top);
left: 24px; left: var(--icon-left);
z-index: 10; z-index: 10;
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
@@ -123,7 +153,7 @@ h1 {
.widget-zone { .widget-zone {
position: absolute; position: absolute;
top: 24px; top: calc(var(--stage-top-offset) + 24px);
right: 24px; right: 24px;
display: grid; display: grid;
gap: 14px; gap: 14px;
@@ -298,14 +328,17 @@ h1 {
.taskbar { .taskbar {
position: fixed; position: fixed;
left: 24px; left: var(--taskbar-left);
right: 24px; right: var(--taskbar-right);
bottom: 24px; bottom: var(--taskbar-bottom);
width: var(--taskbar-width);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
justify-content: var(--taskbar-justify);
min-height: var(--taskbar-height);
padding: 12px 16px; padding: 12px 16px;
border-radius: 22px; border-radius: var(--taskbar-radius);
background: var(--taskbar-bg); background: var(--taskbar-bg);
backdrop-filter: blur(18px); backdrop-filter: blur(18px);
border: 1px solid var(--shell-border); border: 1px solid var(--shell-border);
@@ -322,6 +355,18 @@ h1 {
cursor: pointer; cursor: pointer;
} }
.start-button {
display: inline-flex;
align-items: center;
gap: 8px;
}
.start-button-icon {
font-size: 11px;
font-weight: 700;
opacity: 0.8;
}
.taskbar-app-button.is-focused { .taskbar-app-button.is-focused {
background: var(--accent); background: var(--accent);
color: #0f172a; color: #0f172a;
@@ -334,7 +379,7 @@ h1 {
.start-menu { .start-menu {
position: fixed; position: fixed;
left: 24px; left: 24px;
bottom: 102px; bottom: calc(var(--taskbar-height) + 28px);
z-index: 130; z-index: 130;
display: grid; display: grid;
align-content: start; align-content: start;
@@ -485,6 +530,7 @@ h1 {
left: 16px; left: 16px;
right: 16px; right: 16px;
bottom: 88px; bottom: 88px;
transform: none;
width: auto; width: auto;
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }

View File

@@ -12,10 +12,14 @@ if (payloadNode) {
const startMenuWidgets = document.getElementById('start-menu-widgets'); const startMenuWidgets = document.getElementById('start-menu-widgets');
const startMenuActions = document.getElementById('start-menu-actions'); const startMenuActions = document.getElementById('start-menu-actions');
const traySkins = document.getElementById('tray-skins'); const traySkins = document.getElementById('tray-skins');
const traySkinsTop = document.getElementById('tray-skins-top');
const clockNode = document.getElementById('clock'); const clockNode = document.getElementById('clock');
const clockTopNode = document.getElementById('clock-top');
const windows = new Map(); const windows = new Map();
const activeWidgets = new Set(payload.widgets.active_ids); const activeWidgets = new Set(payload.widgets.active_ids);
const activeSkin = payload.meta.active_skin; const activeSkin = payload.meta.active_skin;
const skinProfile = payload.meta.skin_profile || {};
const maximizeOverSystemBar = Boolean(skinProfile.maximize_over_system_bar);
const storageScope = payload.desktop.storage_scope || 'guest'; const storageScope = payload.desktop.storage_scope || 'guest';
const storageKey = `desktop.kusche.berlin.window-state.${storageScope}`; const storageKey = `desktop.kusche.berlin.window-state.${storageScope}`;
let zIndex = 20; let zIndex = 20;
@@ -57,10 +61,12 @@ if (payloadNode) {
} }
const taskbar = document.querySelector('.taskbar'); const taskbar = document.querySelector('.taskbar');
const systemBar = document.querySelector('.system-bar');
const stageRect = windowsRoot.getBoundingClientRect(); const stageRect = windowsRoot.getBoundingClientRect();
const taskbarRect = taskbar ? taskbar.getBoundingClientRect() : null; const taskbarRect = taskbar ? taskbar.getBoundingClientRect() : null;
const systemBarRect = systemBar ? systemBar.getBoundingClientRect() : null;
topDesktopInset = 0; topDesktopInset = systemBarRect ? Math.round(systemBarRect.height) : 0;
sideDesktopInset = 0; sideDesktopInset = 0;
bottomDesktopInset = taskbarRect bottomDesktopInset = taskbarRect
? Math.max(0, Math.round(stageRect.bottom - taskbarRect.top)) ? Math.max(0, Math.round(stageRect.bottom - taskbarRect.top))
@@ -82,15 +88,19 @@ if (payloadNode) {
}; };
const renderClock = () => { const renderClock = () => {
if (!clockNode) { const formatted = new Intl.DateTimeFormat('de-DE', {
return;
}
clockNode.textContent = new Intl.DateTimeFormat('de-DE', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
second: '2-digit', second: '2-digit',
}).format(new Date()); }).format(new Date());
if (clockNode) {
clockNode.textContent = formatted;
}
if (clockTopNode) {
clockTopNode.textContent = formatted;
}
}; };
const navigateToSkin = (skin) => { const navigateToSkin = (skin) => {
@@ -98,20 +108,22 @@ if (payloadNode) {
}; };
const renderTraySkins = () => { const renderTraySkins = () => {
if (!traySkins) { [traySkins, traySkinsTop].forEach((container) => {
return; if (!container) {
} return;
}
traySkins.innerHTML = ''; container.innerHTML = '';
payload.meta.available_skins.forEach((skin) => { payload.meta.available_skins.forEach((skin) => {
const button = document.createElement('button'); const button = document.createElement('button');
button.type = 'button'; button.type = 'button';
button.className = `tray-skin-button${skin === activeSkin ? ' is-active' : ''}`; button.className = `tray-skin-button${skin === activeSkin ? ' is-active' : ''}`;
button.textContent = skin.slice(0, 3).toUpperCase(); button.textContent = skin.slice(0, 3).toUpperCase();
button.title = skin; button.title = skin;
button.addEventListener('click', () => navigateToSkin(skin)); button.addEventListener('click', () => navigateToSkin(skin));
traySkins.appendChild(button); container.appendChild(button);
});
}); });
}; };
@@ -184,12 +196,13 @@ if (payloadNode) {
record.state.maximized = true; record.state.maximized = true;
record.node.classList.add('is-maximized'); record.node.classList.add('is-maximized');
measureDesktopBounds(); measureDesktopBounds();
const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
record.node.style.left = `${sideDesktopInset}px`; record.node.style.left = `${sideDesktopInset}px`;
record.node.style.top = `${topDesktopInset}px`; record.node.style.top = `${maximizedTopInset}px`;
record.node.style.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`; record.node.style.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`;
record.node.style.height = activeSkin === 'apple' record.node.style.height = maximizeOverSystemBar
? '100%' ? '100%'
: `calc(100% - ${topDesktopInset + bottomDesktopInset}px)`; : `calc(100% - ${maximizedTopInset + bottomDesktopInset}px)`;
focusWindow(windowId); focusWindow(windowId);
saveWindowState(); saveWindowState();
return; return;
@@ -417,12 +430,13 @@ if (payloadNode) {
if (record.state.maximized) { if (record.state.maximized) {
record.node.classList.add('is-maximized'); record.node.classList.add('is-maximized');
measureDesktopBounds(); measureDesktopBounds();
const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
record.node.style.left = `${sideDesktopInset}px`; record.node.style.left = `${sideDesktopInset}px`;
record.node.style.top = `${topDesktopInset}px`; record.node.style.top = `${maximizedTopInset}px`;
record.node.style.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`; record.node.style.width = sideDesktopInset === 0 ? '100%' : `calc(100% - ${sideDesktopInset * 2}px)`;
record.node.style.height = activeSkin === 'apple' record.node.style.height = maximizeOverSystemBar
? '100%' ? '100%'
: `calc(100% - ${topDesktopInset + bottomDesktopInset}px)`; : `calc(100% - ${maximizedTopInset + bottomDesktopInset}px)`;
} }
node.addEventListener('mousedown', () => focusWindow(record.id)); node.addEventListener('mousedown', () => focusWindow(record.id));

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
return [
'id' => 'apple',
'label' => 'Apple',
'family' => 'apple',
'wallpaper' => 'radial-gradient(circle at top, #ffe7ba 0%, #ff9a62 38%, #531b53 100%)',
'top_bar' => true,
'taskbar_mode' => 'dock',
'taskbar_label' => 'Launchpad',
'taskbar_icon' => 'Mac',
'window_controls' => 'mac',
'maximize_over_system_bar' => true,
];

View File

@@ -0,0 +1,50 @@
body[data-skin="apple"] {
--accent: #fb7185;
--taskbar-bg: rgba(45, 45, 45, 0.65);
--window-bg: #fffbeb;
--window-header-bg: #fff1d6;
--desktop-font: "SF Pro Display", "Helvetica Neue", sans-serif;
--taskbar-height: 72px;
--taskbar-left: 50%;
--taskbar-right: auto;
--taskbar-bottom: 18px;
--taskbar-width: auto;
--taskbar-radius: 24px;
--taskbar-justify: center;
--stage-top-offset: 34px;
--icon-top: 56px;
}
body[data-skin="apple"] .system-bar {
background: rgba(244, 244, 245, 0.82);
color: #111827;
}
body[data-skin="apple"] .taskbar {
transform: translateX(-50%);
border: 1px solid rgba(255, 255, 255, 0.26);
box-shadow: 0 18px 40px rgba(17, 24, 39, 0.22);
}
body[data-skin="apple"] .start-button {
order: 2;
min-width: 92px;
justify-content: center;
background: rgba(255, 255, 255, 0.24);
}
body[data-skin="apple"] .tray {
order: 1;
flex: 0 0 auto;
}
body[data-skin="apple"] .taskbar-apps {
order: 3;
justify-content: center;
}
body[data-skin="apple"] .start-menu {
left: 50%;
bottom: 106px;
transform: translateX(-50%);
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
return [
'id' => 'base',
'label' => 'Base',
'family' => 'base',
'wallpaper' => 'linear-gradient(160deg, #1d4ed8 0%, #0f172a 100%)',
'top_bar' => false,
'taskbar_mode' => 'taskbar',
'taskbar_label' => 'Menu',
'taskbar_icon' => 'OS',
'window_controls' => 'windows',
'maximize_over_system_bar' => false,
];

View File

@@ -0,0 +1,24 @@
.tray-skins,
.system-skins {
display: inline-flex;
align-items: center;
gap: 6px;
}
.tray-skin-button {
min-width: 38px;
height: 28px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
color: inherit;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
cursor: pointer;
}
.tray-skin-button.is-active {
background: rgba(255, 255, 255, 0.24);
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
return [
'id' => 'linux',
'label' => 'Linux Mint',
'family' => 'linux-mint',
'wallpaper' => 'linear-gradient(140deg, #123329 0%, #1f5d4f 45%, #0f172a 100%)',
'top_bar' => false,
'taskbar_mode' => 'panel',
'taskbar_label' => 'Menu',
'taskbar_icon' => 'Mint',
'window_controls' => 'linux',
'maximize_over_system_bar' => false,
];

View File

@@ -0,0 +1,33 @@
body[data-skin="linux"] {
--accent: #86efac;
--taskbar-bg: linear-gradient(180deg, rgba(33, 46, 42, 0.98), rgba(21, 31, 28, 0.98));
--window-bg: #f9fafb;
--window-header-bg: #eceff3;
--desktop-font: "Noto Sans", "Ubuntu", sans-serif;
--taskbar-height: 48px;
--taskbar-left: 0;
--taskbar-right: 0;
--taskbar-bottom: 0;
--taskbar-width: 100%;
--taskbar-radius: 0;
--taskbar-justify: flex-start;
}
body[data-skin="linux"] .taskbar {
border-left: 0;
border-right: 0;
border-bottom: 0;
backdrop-filter: none;
box-shadow: 0 -10px 24px rgba(0, 0, 0, 0.16);
}
body[data-skin="linux"] .start-button {
border-radius: 10px;
background: linear-gradient(180deg, rgba(92, 185, 118, 0.24), rgba(55, 110, 75, 0.28));
}
body[data-skin="linux"] .start-menu {
left: 12px;
bottom: 58px;
border-radius: 18px 18px 0 0;
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
return [
'id' => 'windows',
'label' => 'Windows',
'family' => 'windows',
'wallpaper' => 'linear-gradient(160deg, #1d4ed8 0%, #0f172a 100%)',
'top_bar' => false,
'taskbar_mode' => 'taskbar',
'taskbar_label' => 'Start',
'taskbar_icon' => 'Win',
'window_controls' => 'windows',
'maximize_over_system_bar' => false,
];

View File

@@ -0,0 +1,37 @@
body[data-skin="windows"] {
--accent: #60a5fa;
--taskbar-bg: linear-gradient(180deg, rgba(24, 39, 75, 0.94), rgba(15, 23, 42, 0.96));
--window-bg: #f8fafc;
--window-header-bg: linear-gradient(180deg, #eff6ff, #dbeafe);
--desktop-font: "Segoe UI", "IBM Plex Sans", sans-serif;
--taskbar-height: 54px;
--taskbar-left: 0;
--taskbar-right: 0;
--taskbar-bottom: 0;
--taskbar-width: 100%;
--taskbar-radius: 0;
--taskbar-justify: flex-start;
--icon-top: 22px;
--icon-left: 24px;
}
body[data-skin="windows"] .taskbar {
padding: 8px 14px;
border-left: 0;
border-right: 0;
border-bottom: 0;
backdrop-filter: blur(12px);
}
body[data-skin="windows"] .start-button {
min-width: 94px;
justify-content: center;
border-radius: 8px;
background: rgba(255, 255, 255, 0.14);
}
body[data-skin="windows"] .start-menu {
left: 12px;
bottom: 62px;
border-radius: 14px 14px 0 0;
}

View File

@@ -34,6 +34,8 @@ final class App
'title' => 'Kusche.Berlin Desktop UI', 'title' => 'Kusche.Berlin Desktop UI',
'active_skin' => $activeSkin, 'active_skin' => $activeSkin,
'available_skins' => $skins, 'available_skins' => $skins,
'base_skin_profile' => SkinResolver::profile('base'),
'skin_profile' => SkinResolver::profile($activeSkin),
], ],
'desktop' => [ 'desktop' => [
'greeting' => 'Neue Desktop-Shell fuer Kusche.Berlin', 'greeting' => 'Neue Desktop-Shell fuer Kusche.Berlin',

View File

@@ -6,27 +6,131 @@ namespace Desktop;
final class SkinResolver final class SkinResolver
{ {
private const DEFAULT_SKIN = 'windows';
/** /**
* @return array<int, string> * @return array<int, string>
*/ */
public static function all(): array public static function all(): array
{ {
return ['windows', 'apple', 'linux']; return array_values(array_filter(
array_keys(self::profiles()),
static fn (string $skin): bool => $skin !== 'base'
));
} }
public static function resolve(?string $requestedSkin): string public static function resolve(?string $requestedSkin): string
{ {
$skin = strtolower((string) $requestedSkin); $skin = strtolower((string) $requestedSkin);
return in_array($skin, self::all(), true) ? $skin : 'windows'; return in_array($skin, self::all(), true) ? $skin : self::DEFAULT_SKIN;
} }
public static function wallpaper(string $skin): string public static function wallpaper(string $skin): string
{ {
return match ($skin) { return (string) (self::profile($skin)['wallpaper'] ?? '');
'apple' => 'radial-gradient(circle at top, #ffe7ba 0%, #ff9a62 38%, #531b53 100%)', }
'linux' => 'linear-gradient(140deg, #111827 0%, #1f2937 40%, #ea580c 100%)',
default => 'linear-gradient(160deg, #1d4ed8 0%, #0f172a 100%)', /**
}; * @return array<string, array<string, mixed>>
*/
public static function profiles(): array
{
static $profiles;
if (is_array($profiles)) {
return $profiles;
}
$profiles = [];
$skinsPath = self::skinsPath();
$directories = glob($skinsPath . '/*', GLOB_ONLYDIR) ?: [];
foreach ($directories as $directory) {
$skinId = basename($directory);
$profilePath = $directory . '/profile.php';
if (!is_file($profilePath)) {
continue;
}
/** @var array<string, mixed> $profile */
$profile = require $profilePath;
$profiles[$skinId] = array_merge(
[
'id' => $skinId,
'label' => ucfirst($skinId),
'family' => $skinId,
'wallpaper' => '',
'top_bar' => false,
'taskbar_mode' => 'taskbar',
'taskbar_label' => 'Menu',
'taskbar_icon' => strtoupper(substr($skinId, 0, 3)),
'window_controls' => 'windows',
'maximize_over_system_bar' => false,
],
$profile,
self::assetManifest($skinId)
);
}
if (!isset($profiles['base'])) {
$profiles['base'] = array_merge(
[
'id' => 'base',
'label' => 'Base',
'family' => 'base',
'wallpaper' => '',
'top_bar' => false,
'taskbar_mode' => 'taskbar',
'taskbar_label' => 'Menu',
'taskbar_icon' => 'OS',
'window_controls' => 'windows',
'maximize_over_system_bar' => false,
],
self::assetManifest('base')
);
}
return $profiles;
}
/**
* @return array<string, mixed>
*/
public static function profile(string $skin): array
{
$profiles = self::profiles();
$baseProfile = $profiles['base'] ?? [];
if ($skin === 'base') {
return $baseProfile;
}
$resolvedSkin = self::resolve($skin);
return array_merge(
$baseProfile,
$profiles[$resolvedSkin] ?? $profiles[self::DEFAULT_SKIN] ?? $baseProfile
);
}
private static function skinsPath(): string
{
return dirname(__DIR__, 2) . '/public/assets/desktop/skins';
}
/**
* @return array<string, string|null>
*/
private static function assetManifest(string $skin): array
{
$basePath = self::skinsPath() . '/' . $skin;
$publicBasePath = '/assets/desktop/skins/' . $skin;
return [
'css_path' => is_file($basePath . '/skin.css') ? $publicBasePath . '/skin.css' : null,
'js_path' => is_file($basePath . '/skin.js') ? $publicBasePath . '/skin.js' : null,
];
} }
} }