adasd
This commit is contained in:
279
UMSETZUNGSSTATUS.md
Normal file
279
UMSETZUNGSSTATUS.md
Normal 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
46
config/widgets.php
Normal 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.',
|
||||
],
|
||||
];
|
||||
@@ -53,17 +53,42 @@ $wallpaper = $desktopPayload['desktop']['wallpaper'];
|
||||
<main class="desktop-stage">
|
||||
<div class="desktop-icons" id="desktop-icons"></div>
|
||||
|
||||
<div class="widget-zone">
|
||||
<?php foreach ($desktopPayload['widgets'] as $widget): ?>
|
||||
<div class="widget-zone" id="widget-zone">
|
||||
<?php foreach ($desktopPayload['widgets']['active'] as $widget): ?>
|
||||
<article class="widget-card">
|
||||
<div class="widget-card-header">
|
||||
<div>
|
||||
<h3><?= htmlspecialchars($widget['title'], ENT_QUOTES) ?></h3>
|
||||
<p><?= htmlspecialchars($widget['content'], ENT_QUOTES) ?></p>
|
||||
<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>
|
||||
<?php endforeach; ?>
|
||||
</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">
|
||||
<button class="start-button" id="start-button" type="button">Menu</button>
|
||||
<div class="taskbar-apps" id="taskbar-apps"></div>
|
||||
|
||||
@@ -181,6 +181,25 @@ h1 {
|
||||
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 {
|
||||
position: absolute;
|
||||
inset: 24px 24px 92px 24px;
|
||||
@@ -272,6 +291,70 @@ h1 {
|
||||
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 {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -314,6 +397,15 @@ h1 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.start-menu {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 88px;
|
||||
width: auto;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.window-layer {
|
||||
position: static;
|
||||
inset: auto;
|
||||
|
||||
@@ -3,10 +3,17 @@ const payloadNode = document.getElementById('desktop-payload');
|
||||
if (payloadNode) {
|
||||
const payload = JSON.parse(payloadNode.textContent);
|
||||
const iconsRoot = document.getElementById('desktop-icons');
|
||||
const widgetZoneRoot = document.getElementById('widget-zone');
|
||||
const windowsRoot = document.getElementById('window-layer');
|
||||
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 windows = new Map();
|
||||
const activeWidgets = new Set(payload.widgets.active_ids);
|
||||
let zIndex = 20;
|
||||
|
||||
const renderClock = () => {
|
||||
@@ -36,6 +43,15 @@ if (payloadNode) {
|
||||
record.node.classList.add('is-focused');
|
||||
};
|
||||
|
||||
const closeStartMenu = () => {
|
||||
if (!startMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
startMenu.hidden = true;
|
||||
startButton?.setAttribute('aria-expanded', 'false');
|
||||
};
|
||||
|
||||
const syncTaskbar = () => {
|
||||
taskbarRoot.innerHTML = '';
|
||||
|
||||
@@ -152,15 +168,7 @@ if (payloadNode) {
|
||||
syncTaskbar();
|
||||
};
|
||||
|
||||
payload.apps.forEach((app) => {
|
||||
const icon = document.createElement('button');
|
||||
icon.type = 'button';
|
||||
icon.className = 'desktop-icon';
|
||||
icon.innerHTML = `
|
||||
<span class="desktop-icon-badge">${app.icon}</span>
|
||||
<span class="desktop-icon-label">${app.title}</span>
|
||||
`;
|
||||
icon.addEventListener('click', () => {
|
||||
const openApp = (app) => {
|
||||
const existing = Array.from(windows.values()).find((record) => record.definition.app_id === app.app_id);
|
||||
|
||||
if (existing) {
|
||||
@@ -179,10 +187,182 @@ if (payloadNode) {
|
||||
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) => {
|
||||
const icon = document.createElement('button');
|
||||
icon.type = 'button';
|
||||
icon.className = 'desktop-icon';
|
||||
icon.innerHTML = `
|
||||
<span class="desktop-icon-badge">${app.icon}</span>
|
||||
<span class="desktop-icon-label">${app.title}</span>
|
||||
`;
|
||||
icon.addEventListener('click', () => openApp(app));
|
||||
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);
|
||||
renderClock();
|
||||
window.setInterval(renderClock, 1000);
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App;
|
||||
use Desktop\AppRegistry;
|
||||
use Desktop\DesktopState;
|
||||
use Desktop\SkinResolver;
|
||||
use Desktop\WidgetRegistry;
|
||||
use Desktop\WindowManager;
|
||||
|
||||
final class App
|
||||
@@ -22,9 +23,11 @@ final class App
|
||||
public function desktopPayload(): array
|
||||
{
|
||||
$registry = new AppRegistry($this->projectRoot . '/config/apps.php');
|
||||
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php');
|
||||
$skins = SkinResolver::all();
|
||||
$activeSkin = SkinResolver::resolve($_GET['skin'] ?? null);
|
||||
$windows = (new WindowManager($registry))->defaultWindows();
|
||||
$activeWidgetIds = DesktopState::defaultWidgetIds();
|
||||
|
||||
return [
|
||||
'meta' => [
|
||||
@@ -44,8 +47,15 @@ final class App
|
||||
],
|
||||
'apps' => $registry->all(),
|
||||
'windows' => $windows,
|
||||
'widgets' => DesktopState::defaultWidgets(),
|
||||
'widgets' => [
|
||||
'active' => $widgetRegistry->active($activeWidgetIds),
|
||||
'registry' => $widgetRegistry->all(),
|
||||
'active_ids' => $activeWidgetIds,
|
||||
],
|
||||
'tray' => DesktopState::trayItems(),
|
||||
'menu' => [
|
||||
'quick_actions' => DesktopState::quickActions(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 [
|
||||
[
|
||||
'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.',
|
||||
],
|
||||
];
|
||||
return ['public-home', 'personal-links', 'system-status'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,4 +38,28 @@ final class DesktopState
|
||||
['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.',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
40
src/Desktop/WidgetRegistry.php
Normal file
40
src/Desktop/WidgetRegistry.php
Normal 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'] ?? ''])
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user