adasd
All checks were successful
Deploy / deploy-staging (push) Successful in 7s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-07 00:51:49 +02:00
parent 08c0edc68b
commit e1d16d818d
8 changed files with 724 additions and 39 deletions

279
UMSETZUNGSSTATUS.md Normal file
View File

@@ -0,0 +1,279 @@
# Umsetzungsstatus Desktop UI
Stand: 2026-06-07
Diese Datei ist die laufende Arbeitsgrundlage fuer die Umsetzung der Anweisungen aus `Umsetzungsanweisung/`.
Statuslogik:
- `ERLEDIGT`: in sinnvoller Form umgesetzt
- `TEILWEISE`: begonnen oder als Scaffold vorhanden, aber noch nicht fachlich fertig
- `OFFEN`: noch nicht umgesetzt
## Gesamtstatus
Aktuell ist das Projekt auf einem V1-Scaffold-Stand:
- Desktop-Shell auf Root ist vorhanden
- Skin-System `Windows` / `Apple` / `Linux` ist vorbereitet
- einfacher Fenster-Manager ist vorhanden
- App-Registry, Widget-Registry und Import-Basis sind angelegt
- Keycloak ist nur konzeptionell vorbereitet, nicht integriert
- Admin-Bereiche, persistente User-Desktops und echte Modul-Anbindung sind noch offen
## Verbindliche Regeln
- `Old-Nexus/` und die Inhalte unter `Umsetzungsanweisung/Old-Nexus/` sind nur Referenz
- keine Includes, Imports, Asset-Pfade oder Laufzeitkopplung auf Altbestand
- klassische Module bleiben strukturell unter `modules/<modul>/`
- die Desktop-Shell ist eine eigene Anwendungsschicht und kein Theme-Umbau des alten Systems
## Status Nach Datei
### 1. `START_HIER.md`
Status: `TEILWEISE`
Erfuellt:
- neue Desktop-Oberflaeche wird im neuen Projekt aufgebaut
- Zielstruktur wurde angelegt: `public/`, `api/`, `src/`, `config/`, `partials/`, `modules/`, `tools/`, `debug/`, `temp/`
- temporaere Import-Basis liegt unter `temp/nexus-module-import/`
- keine technische Nutzung von `Old-Nexus/` im neuen Laufzeitcode
Offen:
- globale Verwaltungsseiten nur als Platzhalter vorhanden
- klassische Module sind noch nicht schrittweise an die Shell angebunden
- persoenliche Dashboards sind noch nicht funktional
Nachweise:
- [public/index.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/public/index.php:1)
- [temp/nexus-module-import/README.md](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/temp/nexus-module-import/README.md:1)
- [modules/README.md](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/modules/README.md:1)
### 2. `01_ZIELBILD_DESKTOP_UI.md`
Status: `TEILWEISE`
Erfuellt:
- Root `/` ist die Desktop-Oberflaeche
- Desktop-Flaeche, App-Icons, Fenster, Taskbar/Tray, Widget-Bereich und Skin-Konzept sind sichtbar vorhanden
- gemeinsame technische Basis fuer alle drei Skins ist umgesetzt
- funktionales Startmenue fuer Apps, Widgets und Quick Actions ist vorhanden
Offen:
- frei platzierbare Icons sind noch nicht umgesetzt
- persoenliche Startseite und oeffentliches Home sind noch nicht getrennt funktional
- eingebettete Webseiten, Links, externe Integrationen und Dashboard-Inhalte fehlen
Nachweise:
- [partials/desktop/shell.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/partials/desktop/shell.php:1)
- [public/assets/desktop/desktop.css](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/public/assets/desktop/desktop.css:1)
- [public/assets/desktop/desktop.js](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/public/assets/desktop/desktop.js:1)
- [config/widgets.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/config/widgets.php:1)
### 3. `02_ORDNERSTRUKTUR_UND_GRUNDREGELN.md`
Status: `TEILWEISE`
Erfuellt:
- Kernstruktur orientiert sich an Nexus
- `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
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
Nachweise:
- [src/Desktop/AppRegistry.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/src/Desktop/AppRegistry.php:1)
- [src/Desktop/SkinResolver.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/src/Desktop/SkinResolver.php:1)
- [src/Desktop/WindowManager.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/src/Desktop/WindowManager.php:1)
- [src/Desktop/WidgetRegistry.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/src/Desktop/WidgetRegistry.php:1)
### 4. `03_MIGRATION_BESTEHENDE_MODULE.md`
Status: `TEILWEISE`
Erfuellt:
- Rohkopie der Altmodule liegt in `temp/nexus-module-import/modules/`
- relevante Doku aus der Altbasis wurde in den Import-Ordner uebernommen
- Produktivcode referenziert den Import-Ordner nicht zur Laufzeit
Offen:
- keine Modulpruefung je Modul durchgefuehrt
- keine App-Registry-Eintraege fuer echte importierte Module vorhanden
- keine Modulansichten in die Fensterlogik integriert
- kein Skin-Test je Modul erfolgt
Nachweise:
- [temp/nexus-module-import](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/temp/nexus-module-import:1)
- [config/apps.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/config/apps.php:1)
### 5. `04_UMSETZUNGSPLAN_V1.md`
Status: `TEILWEISE`
Phase 1 Fundament:
- `ERLEDIGT`: Projektstruktur
- `ERLEDIGT`: Desktop-Shell als Root
- `ERLEDIGT`: Skin-Vorbereitung
- `TEILWEISE`: Keycloak-Anbindung nur konzeptionell
Phase 2 Desktop-Grundfunktionen:
- `ERLEDIGT`: Desktop-Hintergrund
- `ERLEDIGT`: App-Icons
- `OFFEN`: frei verschiebbare Icons
- `TEILWEISE`: Dock oder Taskbar
- `TEILWEISE`: Statusbereich
- `ERLEDIGT`: Uhr
- `ERLEDIGT`: erstes Hauptmenue mit Funktion
Phase 3 Fenster-Manager:
- `ERLEDIGT`: Fenster oeffnen und schliessen
- `ERLEDIGT`: Fenster fokussieren
- `ERLEDIGT`: Fenster minimieren
- `TEILWEISE`: Fenster maximieren
- `ERLEDIGT`: z-index-Verwaltung
- `OFFEN`: Position und Groesse merken
Phase 4 Inhaltssystem:
- `ERLEDIGT`: App Registry als Grundstruktur
- `ERLEDIGT`: Widget Registry als Grundstruktur
- `OFFEN`: Seitenmodule
- `OFFEN`: persoenliche Linklisten
- `TEILWEISE`: oeffentliches Home-Dashboard nur als Placeholder
- `TEILWEISE`: persoenliche Workspaces nur als Placeholder
Phase 5 Admin-Bereiche:
- `OFFEN`: globale Einstellungen
- `OFFEN`: Widget-Verwaltung
- `OFFEN`: Integrationen
- `OFFEN`: Suchmaschinen
- `TEILWEISE`: Apps nur als Placeholder-App
- `OFFEN`: oeffentliches Home-Dashboard-Verwaltung
Phase 6 Modul-Anbindung:
- `TEILWEISE`: importierte Nexus-Module liegen vor
- `OFFEN`: erste Module als Apps in der Shell
- `OFFEN`: Fenstertauglichkeit je Modul pruefen
- `OFFEN`: langfristige UX-Anpassung je Modul
V1-Minimalziel:
- `ERLEDIGT`: Root ist Desktop-Oberflaeche
- `OFFEN`: Login vorhanden
- `ERLEDIGT`: Desktop-Skins umschaltbar
- `ERLEDIGT`: Apps koennen als Fenster geoeffnet werden
- `OFFEN`: Admin kann global Apps, Widgets, Integrationen und Suchmaschinen verwalten
- `OFFEN`: mindestens ein persoenlicher Desktop pro Nutzer funktioniert
- `TEILWEISE`: oeffentliches Home-Dashboard nur als V1-Placeholder
### 6. `05_KEYCLOAK_LOGIN_UND_DESKTOP_LOGIN.md`
Status: `TEILWEISE`
Erfuellt:
- Desktop-Login ist als eigener App-Eintrag vorbereitet
- Theme-Handoff fuer Keycloak ist dokumentiert
- gestalterische Zielrichtung ist im Projekt festgehalten
Offen:
- keine echte Keycloak-Anbindung
- kein eigenes Keycloak-Theme implementiert
- keine Theme-Zuordnung zu Client oder Realm
- keine Login-UX in der laufenden Anwendung
Nachweise:
- [config/apps.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/config/apps.php:1)
- [docs/keycloak-theme-handoff.md](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/docs/keycloak-theme-handoff.md:1)
- [partials/landingpages/auth/login.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/partials/landingpages/auth/login.php:1)
### 7. `06_STARTPROMPT_NEUE_SESSION.md`
Status: `TEILWEISE`
Erfuellt:
- die dort genannte Erstaufgabe wurde im Scaffold-Sinn ausgefuehrt
- Shell zuerst gebaut, dann Import-Basis vorbereitet
- keine Vermischung mit alter Website-Navigation
Offen:
- Schritt 5 und 6 nur als Grundgeruest, nicht als voll funktionsfaehige Fachumsetzung
## Technischer Ist-Stand
Vorhanden:
- Root-Entry fuer die Desktop-Shell
- JSON-Payload fuer Frontend per `api/desktop.php`
- PHP-Autoload-Basis
- App-Registry
- Widget-Registry
- Skin-Resolver
- Default-Workspace- und Widget-State
- einfacher Window-Manager
- CSS/JS fuer Shell, Fenster, Taskbar, Startmenue, Widgets und Uhr
Hauptdateien:
- [public/index.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/public/index.php:1)
- [api/desktop.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/api/desktop.php:1)
- [src/App/App.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/src/App/App.php:1)
- [src/Desktop/DesktopState.php](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/src/Desktop/DesktopState.php:1)
- [public/assets/desktop/desktop.js](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/public/assets/desktop/desktop.js:1)
## Offene Hauptpunkte
### Prioritaet A
- echtes Login mit Keycloak anbinden
- persoenlichen Desktop-Zustand pro Nutzer speichern
- Admin-App in echte Verwaltungsseiten zerlegen
### Prioritaet B
- frei verschiebbare Desktop-Icons
- Fensterposition und -groesse persistieren
- oeffentliches Home-Dashboard in echte Inhalte ueberfuehren
- persoenliche Linklisten und Workspaces funktional bauen
### Prioritaet C
- erstes importiertes Modul fachlich sichten
- Modul als App registrieren
- Modulansicht in Fenster oeffnen
- Skin- und Fensterverhalten fuer dieses Modul testen
## Arbeitsmodus Fuer Diese Datei
Wenn wir weitere Aufgaben umsetzen, wird diese Datei jeweils aktualisiert:
- Status pro Anweisungsdatei anpassen
- erledigte Punkte von `OFFEN` auf `TEILWEISE` oder `ERLEDIGT` setzen
- neue Nachweise auf konkrete Dateien verlinken
- offene Punkte nur dann als `ERLEDIGT` markieren, wenn sie fachlich funktionieren und nicht nur als Placeholder existieren

46
config/widgets.php Normal file
View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
return [
[
'widget_id' => 'public-home',
'title' => 'Public Home',
'icon' => 'PH',
'zone' => 'sidebar',
'default_enabled' => true,
'supports_public_home' => true,
'summary' => 'Oeffentliches Dashboard fuer nicht eingeloggte Nutzer mit Startinhalten und Systemhinweisen.',
'content' => 'Die erste Ausbaustufe des oeffentlichen Desktops zeigt ein kuratiertes Home-Dashboard mit Kerninhalten und Einstiegspunkten.',
],
[
'widget_id' => 'personal-links',
'title' => 'Persoenliche Links',
'icon' => 'PL',
'zone' => 'sidebar',
'default_enabled' => true,
'supports_public_home' => false,
'summary' => 'Reserviert fuer spaetere nutzerspezifische Schnellzugriffe und Favoriten.',
'content' => 'Hier entstehen spaeter persoenliche Linklisten, Shortcuts und Favoriten pro Workspace.',
],
[
'widget_id' => 'system-status',
'title' => 'Systemstatus',
'icon' => 'SS',
'zone' => 'sidebar',
'default_enabled' => true,
'supports_public_home' => true,
'summary' => 'Zeigt Desktop-Skin, Shell-Status und den aktuellen Ausbaugrad.',
'content' => 'Shell, Fenster-Manager und Registry laufen bereits. Login und Fachmodule folgen spaeter.',
],
[
'widget_id' => 'workspace-notes',
'title' => 'Workspace Notes',
'icon' => 'WN',
'zone' => 'sidebar',
'default_enabled' => false,
'supports_public_home' => false,
'summary' => 'Platz fuer persoenliche Hinweise und Notizen je Workspace.',
'content' => 'Dieses Widget ist vorbereitet, aber standardmaessig noch nicht eingeblendet.',
],
];

View File

@@ -53,17 +53,42 @@ $wallpaper = $desktopPayload['desktop']['wallpaper'];
<main class="desktop-stage"> <main class="desktop-stage">
<div class="desktop-icons" id="desktop-icons"></div> <div class="desktop-icons" id="desktop-icons"></div>
<div class="widget-zone"> <div class="widget-zone" id="widget-zone">
<?php foreach ($desktopPayload['widgets'] as $widget): ?> <?php foreach ($desktopPayload['widgets']['active'] as $widget): ?>
<article class="widget-card"> <article class="widget-card">
<h3><?= htmlspecialchars($widget['title'], ENT_QUOTES) ?></h3> <div class="widget-card-header">
<p><?= htmlspecialchars($widget['content'], ENT_QUOTES) ?></p> <div>
<h3><?= htmlspecialchars($widget['title'], ENT_QUOTES) ?></h3>
<p><?= htmlspecialchars($widget['summary'] ?? '', ENT_QUOTES) ?></p>
</div>
<span class="widget-badge"><?= htmlspecialchars($widget['icon'] ?? 'WG', ENT_QUOTES) ?></span>
</div>
<p><?= htmlspecialchars($widget['content'] ?? '', ENT_QUOTES) ?></p>
</article> </article>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<div class="window-layer" id="window-layer"></div> <div class="window-layer" id="window-layer"></div>
<section class="start-menu" id="start-menu" hidden>
<div class="start-menu-column">
<div class="start-menu-block">
<p class="eyebrow">Quick Actions</p>
<div id="start-menu-actions"></div>
</div>
<div class="start-menu-block">
<p class="eyebrow">Widgets</p>
<div id="start-menu-widgets"></div>
</div>
</div>
<div class="start-menu-column">
<div class="start-menu-block">
<p class="eyebrow">Apps</p>
<div id="start-menu-apps"></div>
</div>
</div>
</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">Menu</button>
<div class="taskbar-apps" id="taskbar-apps"></div> <div class="taskbar-apps" id="taskbar-apps"></div>

View File

@@ -181,6 +181,25 @@ h1 {
box-shadow: var(--shadow); box-shadow: var(--shadow);
} }
.widget-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.widget-badge {
display: inline-grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 12px;
background: rgba(15, 23, 42, 0.58);
font-size: 12px;
font-weight: 700;
}
.window-layer { .window-layer {
position: absolute; position: absolute;
inset: 24px 24px 92px 24px; inset: 24px 24px 92px 24px;
@@ -272,6 +291,70 @@ h1 {
cursor: pointer; cursor: pointer;
} }
.start-menu {
position: absolute;
left: 24px;
bottom: 102px;
z-index: 90;
display: grid;
grid-template-columns: minmax(240px, 280px) minmax(280px, 360px);
gap: 16px;
width: min(720px, calc(100% - 48px));
padding: 18px;
border-radius: 24px;
background: rgba(15, 23, 42, 0.82);
border: 1px solid var(--shell-border);
box-shadow: var(--shadow);
backdrop-filter: blur(24px);
}
.start-menu-column,
.start-menu-block {
display: grid;
gap: 12px;
}
.start-menu-entry {
display: grid;
grid-template-columns: 44px minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
width: 100%;
padding: 12px 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 18px;
background: rgba(255, 255, 255, 0.06);
color: inherit;
text-align: left;
cursor: pointer;
}
.start-menu-entry strong,
.start-menu-entry span {
display: block;
}
.start-menu-entry small {
opacity: 0.7;
}
.start-menu-entry-badge,
.start-menu-entry-toggle {
display: inline-grid;
place-items: center;
min-width: 32px;
height: 32px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
font-size: 12px;
font-weight: 700;
}
.start-menu-entry-toggle.is-active {
background: var(--accent);
color: #0f172a;
}
.taskbar-apps { .taskbar-apps {
display: flex; display: flex;
gap: 10px; gap: 10px;
@@ -314,6 +397,15 @@ h1 {
margin-top: 24px; margin-top: 24px;
} }
.start-menu {
position: fixed;
left: 16px;
right: 16px;
bottom: 88px;
width: auto;
grid-template-columns: 1fr;
}
.window-layer { .window-layer {
position: static; position: static;
inset: auto; inset: auto;

View File

@@ -3,10 +3,17 @@ const payloadNode = document.getElementById('desktop-payload');
if (payloadNode) { if (payloadNode) {
const payload = JSON.parse(payloadNode.textContent); const payload = JSON.parse(payloadNode.textContent);
const iconsRoot = document.getElementById('desktop-icons'); const iconsRoot = document.getElementById('desktop-icons');
const widgetZoneRoot = document.getElementById('widget-zone');
const windowsRoot = document.getElementById('window-layer'); const windowsRoot = document.getElementById('window-layer');
const taskbarRoot = document.getElementById('taskbar-apps'); const taskbarRoot = document.getElementById('taskbar-apps');
const startButton = document.getElementById('start-button');
const startMenu = document.getElementById('start-menu');
const startMenuApps = document.getElementById('start-menu-apps');
const startMenuWidgets = document.getElementById('start-menu-widgets');
const startMenuActions = document.getElementById('start-menu-actions');
const clockNode = document.getElementById('clock'); const clockNode = document.getElementById('clock');
const windows = new Map(); const windows = new Map();
const activeWidgets = new Set(payload.widgets.active_ids);
let zIndex = 20; let zIndex = 20;
const renderClock = () => { const renderClock = () => {
@@ -36,6 +43,15 @@ if (payloadNode) {
record.node.classList.add('is-focused'); record.node.classList.add('is-focused');
}; };
const closeStartMenu = () => {
if (!startMenu) {
return;
}
startMenu.hidden = true;
startButton?.setAttribute('aria-expanded', 'false');
};
const syncTaskbar = () => { const syncTaskbar = () => {
taskbarRoot.innerHTML = ''; taskbarRoot.innerHTML = '';
@@ -152,6 +168,153 @@ if (payloadNode) {
syncTaskbar(); syncTaskbar();
}; };
const openApp = (app) => {
const existing = Array.from(windows.values()).find((record) => record.definition.app_id === app.app_id);
if (existing) {
existing.node.classList.remove('is-minimized');
focusWindow(existing.id);
return;
}
createWindow({
window_id: `window-${app.app_id}-${Date.now()}`,
app_id: app.app_id,
title: app.title,
x: 64 + Math.round(Math.random() * 80),
y: 70 + Math.round(Math.random() * 60),
width: app.default_width || 640,
height: app.default_height || 420,
content: app.summary || '',
});
};
const renderWidgets = () => {
widgetZoneRoot.innerHTML = '';
payload.widgets.registry
.filter((widget) => activeWidgets.has(widget.widget_id))
.forEach((widget) => {
const card = document.createElement('article');
card.className = 'widget-card';
card.innerHTML = `
<div class="widget-card-header">
<div>
<h3>${widget.title}</h3>
<p>${widget.summary || ''}</p>
</div>
<span class="widget-badge">${widget.icon || 'WG'}</span>
</div>
<p>${widget.content || ''}</p>
`;
widgetZoneRoot.appendChild(card);
});
};
const renderMenuWidgets = () => {
startMenuWidgets.innerHTML = '';
payload.widgets.registry.forEach((widget) => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'start-menu-entry';
button.innerHTML = `
<span class="start-menu-entry-badge">${widget.icon || 'WG'}</span>
<span>
<strong>${widget.title}</strong>
<small>${widget.summary || ''}</small>
</span>
<span class="start-menu-entry-toggle ${activeWidgets.has(widget.widget_id) ? 'is-active' : ''}">
${activeWidgets.has(widget.widget_id) ? 'On' : 'Off'}
</span>
`;
button.addEventListener('click', () => {
if (activeWidgets.has(widget.widget_id)) {
activeWidgets.delete(widget.widget_id);
} else {
activeWidgets.add(widget.widget_id);
}
renderWidgets();
renderMenuWidgets();
});
startMenuWidgets.appendChild(button);
});
};
const runQuickAction = (actionId) => {
if (actionId === 'open-public-dashboard') {
const publicDashboard = payload.apps.find((app) => app.app_id === 'public-dashboard');
if (publicDashboard) {
openApp(publicDashboard);
}
closeStartMenu();
return;
}
if (actionId === 'toggle-all-widgets') {
const shouldShowAll = activeWidgets.size !== payload.widgets.registry.length;
activeWidgets.clear();
if (shouldShowAll) {
payload.widgets.registry.forEach((widget) => activeWidgets.add(widget.widget_id));
}
renderWidgets();
renderMenuWidgets();
return;
}
if (actionId === 'cycle-skin') {
const skins = payload.meta.available_skins;
const currentIndex = skins.indexOf(payload.meta.active_skin);
const nextSkin = skins[(currentIndex + 1) % skins.length];
window.location.search = `?skin=${encodeURIComponent(nextSkin)}`;
}
};
const renderStartMenu = () => {
startMenuApps.innerHTML = '';
startMenuActions.innerHTML = '';
payload.apps.forEach((app) => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'start-menu-entry';
button.innerHTML = `
<span class="start-menu-entry-badge">${app.icon}</span>
<span>
<strong>${app.title}</strong>
<small>${app.summary || ''}</small>
</span>
<span class="start-menu-entry-toggle">Open</span>
`;
button.addEventListener('click', () => {
openApp(app);
closeStartMenu();
});
startMenuApps.appendChild(button);
});
payload.menu.quick_actions.forEach((action) => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'start-menu-entry';
button.innerHTML = `
<span class="start-menu-entry-badge">QA</span>
<span>
<strong>${action.label}</strong>
<small>${action.description}</small>
</span>
<span class="start-menu-entry-toggle">Run</span>
`;
button.addEventListener('click', () => runQuickAction(action.action_id));
startMenuActions.appendChild(button);
});
renderMenuWidgets();
};
payload.apps.forEach((app) => { payload.apps.forEach((app) => {
const icon = document.createElement('button'); const icon = document.createElement('button');
icon.type = 'button'; icon.type = 'button';
@@ -160,29 +323,46 @@ if (payloadNode) {
<span class="desktop-icon-badge">${app.icon}</span> <span class="desktop-icon-badge">${app.icon}</span>
<span class="desktop-icon-label">${app.title}</span> <span class="desktop-icon-label">${app.title}</span>
`; `;
icon.addEventListener('click', () => { icon.addEventListener('click', () => openApp(app));
const existing = Array.from(windows.values()).find((record) => record.definition.app_id === app.app_id);
if (existing) {
existing.node.classList.remove('is-minimized');
focusWindow(existing.id);
return;
}
createWindow({
window_id: `window-${app.app_id}-${Date.now()}`,
app_id: app.app_id,
title: app.title,
x: 64 + Math.round(Math.random() * 80),
y: 70 + Math.round(Math.random() * 60),
width: app.default_width || 640,
height: app.default_height || 420,
content: app.summary || '',
});
});
iconsRoot.appendChild(icon); iconsRoot.appendChild(icon);
}); });
startButton?.setAttribute('aria-expanded', 'false');
startButton?.addEventListener('click', () => {
if (!startMenu) {
return;
}
const isOpen = !startMenu.hidden;
startMenu.hidden = isOpen;
startButton.setAttribute('aria-expanded', String(!isOpen));
});
document.addEventListener('click', (event) => {
if (!startMenu || startMenu.hidden) {
return;
}
const target = event.target;
if (!(target instanceof Node)) {
return;
}
if (startMenu.contains(target) || startButton?.contains(target)) {
return;
}
closeStartMenu();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeStartMenu();
}
});
renderWidgets();
renderStartMenu();
payload.windows.forEach(createWindow); payload.windows.forEach(createWindow);
renderClock(); renderClock();
window.setInterval(renderClock, 1000); window.setInterval(renderClock, 1000);

View File

@@ -7,6 +7,7 @@ namespace App;
use Desktop\AppRegistry; use Desktop\AppRegistry;
use Desktop\DesktopState; use Desktop\DesktopState;
use Desktop\SkinResolver; use Desktop\SkinResolver;
use Desktop\WidgetRegistry;
use Desktop\WindowManager; use Desktop\WindowManager;
final class App final class App
@@ -22,9 +23,11 @@ final class App
public function desktopPayload(): array public function desktopPayload(): array
{ {
$registry = new AppRegistry($this->projectRoot . '/config/apps.php'); $registry = new AppRegistry($this->projectRoot . '/config/apps.php');
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php');
$skins = SkinResolver::all(); $skins = SkinResolver::all();
$activeSkin = SkinResolver::resolve($_GET['skin'] ?? null); $activeSkin = SkinResolver::resolve($_GET['skin'] ?? null);
$windows = (new WindowManager($registry))->defaultWindows(); $windows = (new WindowManager($registry))->defaultWindows();
$activeWidgetIds = DesktopState::defaultWidgetIds();
return [ return [
'meta' => [ 'meta' => [
@@ -44,8 +47,15 @@ final class App
], ],
'apps' => $registry->all(), 'apps' => $registry->all(),
'windows' => $windows, 'windows' => $windows,
'widgets' => DesktopState::defaultWidgets(), 'widgets' => [
'active' => $widgetRegistry->active($activeWidgetIds),
'registry' => $widgetRegistry->all(),
'active_ids' => $activeWidgetIds,
],
'tray' => DesktopState::trayItems(), 'tray' => DesktopState::trayItems(),
'menu' => [
'quick_actions' => DesktopState::quickActions(),
],
]; ];
} }
} }

View File

@@ -20,22 +20,11 @@ final class DesktopState
} }
/** /**
* @return array<int, array<string, string>> * @return array<int, string>
*/ */
public static function defaultWidgets(): array public static function defaultWidgetIds(): array
{ {
return [ return ['public-home', 'personal-links', 'system-status'];
[
'widget_id' => 'public-home',
'title' => 'Public Home',
'content' => 'Oeffentliches Dashboard als Startzustand fuer nicht eingeloggte Nutzer.',
],
[
'widget_id' => 'personal-links',
'title' => 'Persoenliche Links',
'content' => 'Platzhalter fuer spaetere user-spezifische Linklisten und Dashboards.',
],
];
} }
/** /**
@@ -49,4 +38,28 @@ final class DesktopState
['id' => 'account', 'label' => 'Gast'], ['id' => 'account', 'label' => 'Gast'],
]; ];
} }
/**
* @return array<int, array<string, string>>
*/
public static function quickActions(): array
{
return [
[
'action_id' => 'open-public-dashboard',
'label' => 'Public Dashboard',
'description' => 'Oeffnet die oeffentliche Startansicht als Fenster.',
],
[
'action_id' => 'toggle-all-widgets',
'label' => 'Widgets umschalten',
'description' => 'Blendet alle registrierten Widgets ein oder aus.',
],
[
'action_id' => 'cycle-skin',
'label' => 'Skin wechseln',
'description' => 'Wechselt zyklisch zwischen Windows, Apple und Linux.',
],
];
}
} }

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Desktop;
final class WidgetRegistry
{
/** @var array<int, array<string, mixed>> */
private array $widgets;
public function __construct(string $configPath)
{
/** @var array<int, array<string, mixed>> $widgets */
$widgets = require $configPath;
$this->widgets = $widgets;
}
/**
* @return array<int, array<string, mixed>>
*/
public function all(): array
{
return $this->widgets;
}
/**
* @param array<int, string> $widgetIds
* @return array<int, array<string, mixed>>
*/
public function active(array $widgetIds): array
{
$lookup = array_fill_keys($widgetIds, true);
return array_values(array_filter(
$this->widgets,
static fn (array $widget): bool => isset($lookup[$widget['widget_id'] ?? ''])
));
}
}