UI Skins
This commit is contained in:
@@ -73,6 +73,7 @@ Offen:
|
||||
|
||||
- frei platzierbare Icons sind noch nicht umgesetzt
|
||||
- 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
|
||||
- eingebettete Webseiten, Links, externe Integrationen und Dashboard-Inhalte fehlen
|
||||
|
||||
@@ -93,12 +94,14 @@ Erfuellt:
|
||||
- `src/Desktop/` ist als globaler Desktop-Core vorhanden
|
||||
- `public/assets/desktop/` und `partials/desktop/` sind angelegt
|
||||
- Trennung zwischen globalem System, Desktop-UI und Modulen ist strukturell vorbereitet
|
||||
- Skin-System wird auf gemeinsame Basis plus Skin-spezifische Ordnerstruktur vorbereitet
|
||||
|
||||
Offen:
|
||||
|
||||
- `modules/<modul>/` im neuen Projekt enthalten noch keine wirklich angebundenen Module
|
||||
- `partials/landingpages/` und `partials/structure/` sind nur minimal vorbereitet
|
||||
- `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:
|
||||
|
||||
@@ -149,6 +152,7 @@ Phase 2 Desktop-Grundfunktionen:
|
||||
- `TEILWEISE`: Statusbereich
|
||||
- `ERLEDIGT`: Uhr
|
||||
- `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:
|
||||
|
||||
@@ -156,6 +160,7 @@ Phase 3 Fenster-Manager:
|
||||
- `ERLEDIGT`: Fenster fokussieren
|
||||
- `ERLEDIGT`: Fenster minimieren
|
||||
- `ERLEDIGT`: Fenster maximieren
|
||||
- `ERLEDIGT`: Fenster-Layout und deckende Darstellung
|
||||
- `ERLEDIGT`: z-index-Verwaltung
|
||||
- `TEILWEISE`: Position und Groesse merken, technisch vorbereitet aber noch nicht abgenommen
|
||||
|
||||
@@ -258,6 +263,7 @@ Hauptdateien:
|
||||
|
||||
### Prioritaet A
|
||||
|
||||
- echte OS-Skins mit eigenem Layout und Verhalten fuer Windows, Apple und Linux Mint fertigziehen
|
||||
- echtes Login mit Keycloak anbinden
|
||||
- persoenlichen Desktop-Zustand pro Nutzer speichern
|
||||
- Admin-App in echte Verwaltungsseiten zerlegen
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
$payloadJson = json_encode($desktopPayload, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
|
||||
$activeSkin = $desktopPayload['meta']['active_skin'];
|
||||
$baseSkinProfile = $desktopPayload['meta']['base_skin_profile'];
|
||||
$skinProfile = $desktopPayload['meta']['skin_profile'];
|
||||
$wallpaper = $desktopPayload['desktop']['wallpaper'];
|
||||
?><!DOCTYPE html>
|
||||
<html lang="de">
|
||||
@@ -12,9 +14,37 @@ $wallpaper = $desktopPayload['desktop']['wallpaper'];
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= htmlspecialchars($desktopPayload['meta']['title'], ENT_QUOTES) ?></title>
|
||||
<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>
|
||||
<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">
|
||||
<?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">
|
||||
<div class="desktop-icons" id="desktop-icons"></div>
|
||||
|
||||
@@ -61,7 +91,10 @@ $wallpaper = $desktopPayload['desktop']['wallpaper'];
|
||||
</section>
|
||||
|
||||
<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="tray">
|
||||
<?php foreach ($desktopPayload['tray'] as $trayItem): ?>
|
||||
@@ -76,5 +109,11 @@ $wallpaper = $desktopPayload['desktop']['wallpaper'];
|
||||
|
||||
<script id="desktop-payload" type="application/json"><?= $payloadJson ?></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>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--shell-text: #f8fafc;
|
||||
--shell-panel: rgba(15, 23, 42, 0.42);
|
||||
--shell-border: rgba(255, 255, 255, 0.16);
|
||||
--card-bg: rgba(255, 255, 255, 0.14);
|
||||
--window-bg: #f8fafc;
|
||||
@@ -10,7 +9,18 @@
|
||||
--accent: #60a5fa;
|
||||
--taskbar-bg: rgba(15, 23, 42, 0.8);
|
||||
--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);
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
margin: 0 0 12px;
|
||||
text-transform: uppercase;
|
||||
@@ -79,8 +109,8 @@ h1 {
|
||||
|
||||
.desktop-icons {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
left: 24px;
|
||||
top: var(--icon-top);
|
||||
left: var(--icon-left);
|
||||
z-index: 10;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
||||
@@ -123,7 +153,7 @@ h1 {
|
||||
|
||||
.widget-zone {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
top: calc(var(--stage-top-offset) + 24px);
|
||||
right: 24px;
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
@@ -298,14 +328,17 @@ h1 {
|
||||
|
||||
.taskbar {
|
||||
position: fixed;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
left: var(--taskbar-left);
|
||||
right: var(--taskbar-right);
|
||||
bottom: var(--taskbar-bottom);
|
||||
width: var(--taskbar-width);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
justify-content: var(--taskbar-justify);
|
||||
min-height: var(--taskbar-height);
|
||||
padding: 12px 16px;
|
||||
border-radius: 22px;
|
||||
border-radius: var(--taskbar-radius);
|
||||
background: var(--taskbar-bg);
|
||||
backdrop-filter: blur(18px);
|
||||
border: 1px solid var(--shell-border);
|
||||
@@ -322,6 +355,18 @@ h1 {
|
||||
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 {
|
||||
background: var(--accent);
|
||||
color: #0f172a;
|
||||
@@ -334,7 +379,7 @@ h1 {
|
||||
.start-menu {
|
||||
position: fixed;
|
||||
left: 24px;
|
||||
bottom: 102px;
|
||||
bottom: calc(var(--taskbar-height) + 28px);
|
||||
z-index: 130;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
@@ -485,6 +530,7 @@ h1 {
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 88px;
|
||||
transform: none;
|
||||
width: auto;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -12,10 +12,14 @@ if (payloadNode) {
|
||||
const startMenuWidgets = document.getElementById('start-menu-widgets');
|
||||
const startMenuActions = document.getElementById('start-menu-actions');
|
||||
const traySkins = document.getElementById('tray-skins');
|
||||
const traySkinsTop = document.getElementById('tray-skins-top');
|
||||
const clockNode = document.getElementById('clock');
|
||||
const clockTopNode = document.getElementById('clock-top');
|
||||
const windows = new Map();
|
||||
const activeWidgets = new Set(payload.widgets.active_ids);
|
||||
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 storageKey = `desktop.kusche.berlin.window-state.${storageScope}`;
|
||||
let zIndex = 20;
|
||||
@@ -57,10 +61,12 @@ if (payloadNode) {
|
||||
}
|
||||
|
||||
const taskbar = document.querySelector('.taskbar');
|
||||
const systemBar = document.querySelector('.system-bar');
|
||||
const stageRect = windowsRoot.getBoundingClientRect();
|
||||
const taskbarRect = taskbar ? taskbar.getBoundingClientRect() : null;
|
||||
const systemBarRect = systemBar ? systemBar.getBoundingClientRect() : null;
|
||||
|
||||
topDesktopInset = 0;
|
||||
topDesktopInset = systemBarRect ? Math.round(systemBarRect.height) : 0;
|
||||
sideDesktopInset = 0;
|
||||
bottomDesktopInset = taskbarRect
|
||||
? Math.max(0, Math.round(stageRect.bottom - taskbarRect.top))
|
||||
@@ -82,15 +88,19 @@ if (payloadNode) {
|
||||
};
|
||||
|
||||
const renderClock = () => {
|
||||
if (!clockNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
clockNode.textContent = new Intl.DateTimeFormat('de-DE', {
|
||||
const formatted = new Intl.DateTimeFormat('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}).format(new Date());
|
||||
|
||||
if (clockNode) {
|
||||
clockNode.textContent = formatted;
|
||||
}
|
||||
|
||||
if (clockTopNode) {
|
||||
clockTopNode.textContent = formatted;
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToSkin = (skin) => {
|
||||
@@ -98,11 +108,12 @@ if (payloadNode) {
|
||||
};
|
||||
|
||||
const renderTraySkins = () => {
|
||||
if (!traySkins) {
|
||||
[traySkins, traySkinsTop].forEach((container) => {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
traySkins.innerHTML = '';
|
||||
container.innerHTML = '';
|
||||
|
||||
payload.meta.available_skins.forEach((skin) => {
|
||||
const button = document.createElement('button');
|
||||
@@ -111,7 +122,8 @@ if (payloadNode) {
|
||||
button.textContent = skin.slice(0, 3).toUpperCase();
|
||||
button.title = skin;
|
||||
button.addEventListener('click', () => navigateToSkin(skin));
|
||||
traySkins.appendChild(button);
|
||||
container.appendChild(button);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -184,12 +196,13 @@ if (payloadNode) {
|
||||
record.state.maximized = true;
|
||||
record.node.classList.add('is-maximized');
|
||||
measureDesktopBounds();
|
||||
const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
|
||||
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.height = activeSkin === 'apple'
|
||||
record.node.style.height = maximizeOverSystemBar
|
||||
? '100%'
|
||||
: `calc(100% - ${topDesktopInset + bottomDesktopInset}px)`;
|
||||
: `calc(100% - ${maximizedTopInset + bottomDesktopInset}px)`;
|
||||
focusWindow(windowId);
|
||||
saveWindowState();
|
||||
return;
|
||||
@@ -417,12 +430,13 @@ if (payloadNode) {
|
||||
if (record.state.maximized) {
|
||||
record.node.classList.add('is-maximized');
|
||||
measureDesktopBounds();
|
||||
const maximizedTopInset = maximizeOverSystemBar ? 0 : topDesktopInset;
|
||||
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.height = activeSkin === 'apple'
|
||||
record.node.style.height = maximizeOverSystemBar
|
||||
? '100%'
|
||||
: `calc(100% - ${topDesktopInset + bottomDesktopInset}px)`;
|
||||
: `calc(100% - ${maximizedTopInset + bottomDesktopInset}px)`;
|
||||
}
|
||||
|
||||
node.addEventListener('mousedown', () => focusWindow(record.id));
|
||||
|
||||
16
public/assets/desktop/skins/apple/profile.php
Normal file
16
public/assets/desktop/skins/apple/profile.php
Normal 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,
|
||||
];
|
||||
50
public/assets/desktop/skins/apple/skin.css
Normal file
50
public/assets/desktop/skins/apple/skin.css
Normal 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%);
|
||||
}
|
||||
16
public/assets/desktop/skins/base/profile.php
Normal file
16
public/assets/desktop/skins/base/profile.php
Normal 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,
|
||||
];
|
||||
24
public/assets/desktop/skins/base/skin.css
Normal file
24
public/assets/desktop/skins/base/skin.css
Normal 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);
|
||||
}
|
||||
16
public/assets/desktop/skins/linux/profile.php
Normal file
16
public/assets/desktop/skins/linux/profile.php
Normal 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,
|
||||
];
|
||||
33
public/assets/desktop/skins/linux/skin.css
Normal file
33
public/assets/desktop/skins/linux/skin.css
Normal 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;
|
||||
}
|
||||
16
public/assets/desktop/skins/windows/profile.php
Normal file
16
public/assets/desktop/skins/windows/profile.php
Normal 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,
|
||||
];
|
||||
37
public/assets/desktop/skins/windows/skin.css
Normal file
37
public/assets/desktop/skins/windows/skin.css
Normal 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;
|
||||
}
|
||||
@@ -34,6 +34,8 @@ final class App
|
||||
'title' => 'Kusche.Berlin Desktop UI',
|
||||
'active_skin' => $activeSkin,
|
||||
'available_skins' => $skins,
|
||||
'base_skin_profile' => SkinResolver::profile('base'),
|
||||
'skin_profile' => SkinResolver::profile($activeSkin),
|
||||
],
|
||||
'desktop' => [
|
||||
'greeting' => 'Neue Desktop-Shell fuer Kusche.Berlin',
|
||||
|
||||
@@ -6,27 +6,131 @@ namespace Desktop;
|
||||
|
||||
final class SkinResolver
|
||||
{
|
||||
private const DEFAULT_SKIN = 'windows';
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
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
|
||||
{
|
||||
$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
|
||||
{
|
||||
return match ($skin) {
|
||||
'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 (string) (self::profile($skin)['wallpaper'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user