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
- 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

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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,20 +108,22 @@ if (payloadNode) {
};
const renderTraySkins = () => {
if (!traySkins) {
return;
}
[traySkins, traySkinsTop].forEach((container) => {
if (!container) {
return;
}
traySkins.innerHTML = '';
container.innerHTML = '';
payload.meta.available_skins.forEach((skin) => {
const button = document.createElement('button');
button.type = 'button';
button.className = `tray-skin-button${skin === activeSkin ? ' is-active' : ''}`;
button.textContent = skin.slice(0, 3).toUpperCase();
button.title = skin;
button.addEventListener('click', () => navigateToSkin(skin));
traySkins.appendChild(button);
payload.meta.available_skins.forEach((skin) => {
const button = document.createElement('button');
button.type = 'button';
button.className = `tray-skin-button${skin === activeSkin ? ' is-active' : ''}`;
button.textContent = skin.slice(0, 3).toUpperCase();
button.title = skin;
button.addEventListener('click', () => navigateToSkin(skin));
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));

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',
'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',

View File

@@ -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,
];
}
}