WIP: Vendoo 1.44.0 – UX Foundation & Guided Workspaces #29

Draft
Masterluke77 wants to merge 284 commits from release/1.44.0-ux-foundation-production-update-v1 into main
64 changed files with 15474 additions and 9 deletions
+24 -1
View File
@@ -34,6 +34,9 @@ jobs:
- name: Abhängigkeiten reproduzierbar installieren
run: npm ci
- name: Navigation API und Safe Actions früh prüfen
run: npm run verify:navigation-api && npm run verify:safe-action-adapters
- name: Git-Sicherheitsgate
run: npm run verify:git
@@ -144,6 +147,14 @@ jobs:
- name: Abhängigkeiten reproduzierbar installieren
run: npm ci
- name: Navigation API und Safe Actions früh prüfen
shell: pwsh
run: |
& npm.cmd run verify:navigation-api
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
& npm.cmd run verify:safe-action-adapters
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Alle PowerShell-Dateien mit Windows PowerShell 5.1 parsen
shell: powershell
run: .\tools\verify-powershell-parser.ps1 -ReportPath "$env:RUNNER_TEMP\powershell-parser-diagnostics.txt"
@@ -159,7 +170,19 @@ jobs:
- name: Vollständigen Vendoo-Releasevertrag unter Windows ausführen
shell: pwsh
run: npm run verify:all
run: |
& npm.cmd run verify:all 2>&1 | Tee-Object -FilePath "$env:RUNNER_TEMP\windows-release-contract.txt"
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) { exit $exitCode }
- name: Windows-Releasevertrag bei Fehler sichern
if: failure()
uses: actions/upload-artifact@v4
with:
name: windows-release-contract-${{ github.event.pull_request.head.sha || github.sha }}
path: ${{ runner.temp }}/windows-release-contract.txt
if-no-files-found: warn
retention-days: 14
package-after-acceptance:
name: Updater-3.4-Paket nach Abnahme
@@ -0,0 +1,43 @@
name: Vendoo UX Platform Contract
on:
push:
branches: [main, develop, 'feature/**', 'release/**', 'hotfix/**']
pull_request:
branches: [main, develop]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: vendoo-ux-platform-${{ github.ref }}
cancel-in-progress: true
jobs:
ux-platform-contract:
name: UX Platform (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- name: Exakten Commit ohne Marketplace-Action auschecken
run: |
git init .
git remote add origin "${{ github.server_url }}/${{ github.repository }}.git"
git fetch --depth=1 origin "${{ github.sha }}"
git checkout --detach FETCH_HEAD
- name: Runner-Node und Plattform dokumentieren
run: |
node -e "const report=['Platform: '+process.platform,'Node: '+process.version,'Architecture: '+process.arch,'Commit: '+process.env.GITHUB_SHA,''].join('\\n'); process.stdout.write(report);"
- name: Navigation API und Safe Actions prüfen
run: node tools/run-ux-platform-gates.mjs
- name: Diagnose immer ausgeben
if: always()
run: node -e "const fs=require('fs'); for (const file of ['artifacts/ux-platform-preflight.log','artifacts/ux-platform-gates.log']) { if (fs.existsSync(file)) { console.log('--- '+file+' ---'); process.stdout.write(fs.readFileSync(file,'utf8')); } }"
@@ -0,0 +1,70 @@
# Vendoo 1.44.0 UX Slice 16: Command Palette Registry Migration
## Referenz
- Branch: `release/1.44.0-ux-foundation-production-update-v1`
- Ausgangs-Head: `eabbafbebd9288a258971c0a0a8abbeb27c9ea20`
- App-Ziel: Vendoo `1.44.0`
- Updater: `3.4.0`
## Problem
Die globale Ctrl+K-Palette führte vor diesem Slice mit `VIEW_LABELS` und `collectCommands()` eine zweite statische Command-Quelle. Dadurch konnten Registry-Metadaten, Modulregistrierungen, Aliase, Berechtigungen, deaktivierte Zustände, Favoriten und zuletzt geöffnete Bereiche nicht vollständig und konsistent ausgewertet werden.
## Umsetzung
- `window.VendooCommands` bleibt alleinige Datenquelle der Palette.
- Der bestehende Command-Vertrag wurde ausschließlich additiv um `aliases`, `visible`, `enabled` und `disabledReason` ergänzt.
- `list(...)` löst Sichtbarkeit, Permission und Enablement vor der Darstellung auf.
- Suchranking gewichtet Titel, Aliase, Keywords, Beschreibung und Gruppe; Favoriten und Recents liefern kontrollierte Ranking-Boosts.
- `execute(...)` prüft den Command-Zustand unmittelbar vor der Ausführung erneut und emittiert bei Ablehnung `vendoo:command-blocked`.
- Die Palette ruft ausschließlich `VendooCommands.execute(...)` auf. Sie führt keine direkte View-Navigation und keine fachliche Aktion aus.
- Browser-History, Deep Links und Back/Forward bleiben unverändert im bestehenden globalen Navigationslayer.
## Accessibility und UX
- vollständige Tastaturnavigation mit Pfeiltasten, Home, End, Enter und Escape
- Combobox-/Listbox-Vertrag mit `aria-activedescendant`
- verständliche Anzeige deaktivierter Commands einschließlich Begründung
- Fokusfalle im Dialog und verlässliche Rückgabe an den Auslöser
- Live-Status für Trefferzahl, Auswahl und blockierte Commands
- responsive Darstellung bis 320 px, Reduced Motion und 200-%-Zoom-taugliche Struktur
- Ctrl+K überschreibt keine Eingabe- oder Contenteditable-Kontexte, solange die Palette geschlossen ist
## Daten-, Security- und Betriebsgrenzen
- keine Datenbankänderung
- keine Migration
- keine Route, API oder Modul-ID umbenannt
- keine Netzwerkaufrufe aus Command Platform oder Palette
- keine automatische Speicherung, Veröffentlichung, Installation, Deaktivierung oder Löschung
- explizite Permission ohne auflösbaren Permission-Resolver wird in der UI nach dem Deny-by-default-Prinzip nicht dargestellt
- serverseitige Berechtigungsprüfung bleibt maßgeblich
## Verifikation
Neues Gate: `npm run verify:command-palette-registry`
Das Gate prüft:
- Registry als alleinige Quelle
- Wegfall von `VIEW_LABELS` und `collectCommands()`
- Zustandsauflösung, Aliase und Ranking
- Favoriten- und Recents-Integration
- sichere Registry-Ausführung
- Tastatur-, Fokus- und ARIA-Verträge
- Loader-Reihenfolge
- Ausschluss von Netzwerk-, Schreib-, Polling- und direkten Produktivaktionen
- Einbindung in `verify:all`
## Rollback
Rollback erfolgt durch Rücknahme der Änderungen an:
- `public/command-platform.js`
- `public/global-navigation-ux.js`
- `public/global-navigation-ux.css`
- `tools/verify-command-palette-registry-1.44.0.mjs`
- `package.json`
Persistente Fach- oder Benutzerdaten werden durch diesen Slice nicht verändert. Die bestehenden Local-Storage-Keys für Favoriten und Recents bleiben unverändert.
@@ -0,0 +1,144 @@
# Vendoo 1.44.0 UX Slice 17: Universal Action Registry
## Referenz
- Branch: `release/1.44.0-ux-foundation-production-update-v1`
- Ausgangs-Head: `7bc8a81bba5a220f4dda5bcc6911d40060c257a5`
- App-Ziel: Vendoo `1.44.0`
- Updater: `3.4.0`
## Ziel
Vendoo erhält eine zentrale, erweiterbare Action-Plattform. Fachmodule und UX-Workspaces registrieren Aktionen künftig einmalig. Aus demselben Vertrag können Command Palette, Toolbars, Kontextmenüs, Mobile Action Sheets, Shortcuts, Mehrfachauswahl und Plugin-Surfaces gespeist werden, ohne Handler oder Berechtigungslogik zu duplizieren.
## Öffentliche API
`window.VendooActions`
- `register(action, options)`
- `registerMany(actions, options)`
- `unregister(id)`
- `get(id)`
- `list(options)`
- `resolve(id, context)`
- `canExecute(id, context)`
- `execute(id, context)`
- `refresh(id?)`
- `renderSurface(container, options)`
- `mountSurface(container, options)`
- `normalizeShortcut(shortcut)`
- `setConfirmationProvider(provider)`
- `setAuditSink(sink)`
- `subscribe(listener)`
## Action-Vertrag
Verbindliche Felder:
- `id`
- `title`
- `handler`
Unterstützte Metadaten:
- `description`, `icon`, `group`
- `aliases`, `keywords`
- `surfaces`, `shortcut`, `priority`
- `permission`, `visible`, `enabled`, `disabledReason`
- `contexts`, `selection.min`, `selection.max`
- `danger`: `none`, `caution`, `destructive`
- `confirm`
- `concurrency`: `single` oder `parallel`
- `allowInInput`
- `rollback`
- `commandId`
Doppelte Action-IDs werden standardmäßig abgewiesen. Eine bewusste Ersetzung ist nur mit `{ replace: true }` möglich.
## Sicherheitsmodell
- Permissions werden vor Darstellung und unmittelbar vor Ausführung geprüft.
- Eine deklarierte Permission ohne verfügbaren Resolver wird nach Deny-by-default nicht freigegeben.
- Destruktive Actions benötigen immer eine zusätzliche Bestätigung.
- `window.confirm` wird nicht verwendet; die Standardbestätigung ist ein barrierearmer Vendoo-Dialog.
- Optional kann eine Bestätigungsphrase verlangt werden.
- Single-Flight verhindert unbeabsichtigte Doppelausführungen.
- Ein optionaler Rollback-Hook wird bei fehlgeschlagenen Actions kontrolliert ausgeführt.
- Registry und UI-Surfaces implementieren keine Fachlogik und keine direkten Netzwerkaufrufe.
- Serverseitige Berechtigungs-, CSRF- und Validierungsregeln bleiben maßgeblich.
## Plattformintegration
Actions mit der Surface `command-palette` werden automatisch als `kind: action` in `window.VendooCommands` registriert. Die Ctrl+K-Palette bleibt dadurch weiterhin vollständig Registry-getrieben.
Statusänderungen wie Running-, Selection-, Permission- oder Modulzustände aktualisieren den bestehenden Command-Adapter ohne vorherige Abmeldung. Dadurch bleiben favorisierte Action-Commands bei einem normalen Registry-Refresh erhalten. Eine Abmeldung erfolgt ausschließlich bei echter Action-Entfernung oder einer bewussten Ersetzung.
Deklarative UI-Elemente können mit `data-vendoo-action="<action-id>"` gebunden werden. Die Registry synchronisiert `disabled`, `aria-disabled`, Running- und Danger-Zustände. Dynamische Surfaces können über `renderSurface(...)` oder `mountSurface(...)` aufgebaut werden.
## Events und Audit-Anbindung
Die Registry emittiert unter anderem:
- `vendoo:actions-ready`
- `vendoo:actions-changed`
- `vendoo:action-state-changed`
- `vendoo:action-executing`
- `vendoo:action-executed`
- `vendoo:action-failed`
- `vendoo:action-blocked`
- `vendoo:action-cancelled`
- `vendoo:action-rolled-back`
- `vendoo:action-rollback-failed`
- `vendoo:action-shortcut-conflict`
Ein optionaler Audit Sink kann diese bereits bereinigten Lifecycle-Ereignisse übernehmen. Die Registry selbst sendet keine Telemetrie und speichert keine fachlichen Nutzdaten.
## Accessibility und UX
- semantische Buttons und ARIA-Zustände
- vollständige Tastaturbedienung
- Shortcuts mit Konflikterkennung und Prioritäten
- keine Shortcut-Ausführung in Eingabefeldern ohne explizites `allowInInput`
- Fokusfalle und Fokus-Rückgabe im Bestätigungsdialog
- responsive Toolbar-, Menü- und Sheet-Surfaces
- Reduced-Motion-Unterstützung
- Theme-Token statt fest verdrahteter Produktfarben
## Verifikation
Neues Gate: `npm run verify:universal-actions`
Das Gate prüft statisch und in einer isolierten Runtime:
- öffentlichen Registry-Vertrag
- Command-Palette-Adapter
- Erhalt registrierter Command-Adapter und Favoriten bei Status-Refreshes
- Kontext-, Permission- und Auswahlauflösung
- Duplicate-ID-Schutz
- destruktive Bestätigung
- Single-Flight
- Rollback
- Lifecycle-Events
- Shortcut-Normalisierung
- DOM-/Surface-Vertrag
- Asset-Ladereihenfolge
- Ausschluss von Netzwerk-, Polling-, innerHTML- und direkten Fachaktionen
- Einbindung in `verify:all`
## Daten-, Release- und Rollback-Grenzen
- keine Datenbankänderung
- keine Migration
- keine Route, API oder Modul-ID umbenannt
- keine bestehende Fachaktion automatisch migriert oder ausgelöst
- kein Merge, Deployment oder Release
Rollback erfolgt durch Rücknahme von:
- `public/action-registry.js`
- `public/action-registry.css`
- Loader-Erweiterung in `public/module-extension-workspace.js`
- `tools/verify-universal-action-registry-1.44.0.mjs`
- `package.json`
Persistente Artikel-, Medien-, Benutzer-, Modul-, Publishing- oder Einstellungsdaten werden nicht verändert.
@@ -0,0 +1,127 @@
# Vendoo 1.44.0 UX Slice 18: Safe Action Adapters
## Referenz
- Branch: `release/1.44.0-ux-foundation-production-update-v1`
- Ausgangs-Head: `bd87f739992085bf0d06060b9b075bb5d2219175`
- App-Ziel: Vendoo `1.44.0`
- Updater: `3.4.0`
## Ziel
Die Universal Action Registry aus Slice 17 wird erstmals mit bestehenden Vendoo-Funktionen verbunden. Der Slice migriert ausschließlich nicht destruktive Aktionen. Jeder Adapter delegiert an einen bereits vorhandenen, geprüften DOM-Handler und enthält keine neue Fach-, Netzwerk- oder Persistenzlogik.
## Migrierte Aktionen
### Module & Extensions
- `workspace:modules:refresh`
- ruft ausschließlich den vorhandenen Button `#module-manager-refresh` auf
- `workspace:modules:filter-all`
- delegiert an den vorhandenen Modulfilter `all`
- `workspace:modules:filter-problems`
- delegiert an den vorhandenen Modulfilter `problems`
### Artikel
- `workspace:articles:open-current`
- öffnet den zuletzt fokussierten oder ausgewählten Artikel über `.history-article-title`
- `workspace:articles:view-cards`
- delegiert an die bestehende Kartenansicht
- `workspace:articles:view-list`
- delegiert an die bestehende Listenansicht
### Medien
- `workspace:media:open-current-preview`
- öffnet das zuletzt fokussierte oder ausgewählte Bild über den vorhandenen Preview-Handler
- `workspace:media:density-comfortable`
- delegiert an die vorhandene mittlere Galeriedichte
- `workspace:media:reset-filters`
- delegiert an den vorhandenen Medienfilter-Reset
## Kontextmodell
Artikel- und Medienaktionen arbeiten nicht mit willkürlich gewählten Datensätzen. Der Adapter merkt sich ausschließlich den zuletzt per Fokus oder Pointer adressierten Artikel beziehungsweise die zuletzt adressierte Medienvorschau.
Falls kein solcher Kontext vorhanden ist, kann alternativ eine bereits ausgewählte Zeile beziehungsweise Medienkarte verwendet werden. Ohne eindeutigen Kontext bleibt die Action deaktiviert.
Zusätzlich wird jede Action gegen `window.VendooNavigation.currentView` geprüft. Modulaktionen sind nur in `admin`, Artikelaktionen nur in `history` und Medienaktionen nur in `media-library` sichtbar. Ein technisch noch vorhandener, aber inaktiver DOM-Bereich reicht ausdrücklich nicht aus.
## Command-Palette-Integration
Alle neun Adapter werden über `window.VendooActions.registerMany(...)` registriert und nutzen ausschließlich die Surface `command-palette`.
Dadurch erscheinen sie in der bestehenden Ctrl+K-Palette, ohne eine zweite Command-Liste einzuführen. Sichtbarkeit und Enablement werden unmittelbar vor Darstellung und Ausführung erneut gegen den aktuellen DOM- und Workspace-Kontext geprüft.
## Sicherheitsgrenzen
- ausschließlich `danger: none`
- keine destruktiven oder vorsichtigen Actions
- keine Veröffentlichung
- keine Löschung
- keine Installation oder Deinstallation
- keine Modulaktivierung oder -deaktivierung
- keine Statusänderung
- keine Speicherung
- keine Datenbankänderung
- keine Migration
- keine neuen API-Aufrufe
- kein Polling
- kein `window.confirm`
- kein `innerHTML`
Die Adapter rufen nur vorhandene Buttons oder Links per `click()` auf. Serverseitige Berechtigungen, CSRF-Schutz, Validierungen und bestehende Handler bleiben unverändert maßgeblich.
## Asset-Ladereihenfolge
`public/module-extension-workspace.js` lädt jetzt in dieser Reihenfolge:
1. Command Platform
2. Universal Action Registry
3. Global Navigation
4. Safe Action Adapters
Der Adapter wartet zusätzlich auf `vendoo:actions-ready`, falls die Action Registry beim Laden noch nicht verfügbar ist.
## Verifikation
Neues Gate:
```bash
npm run verify:safe-action-adapters
```
Das Gate prüft statisch und in einer isolierten Runtime:
- alle neun stabilen Action-IDs
- ausschließliche Command-Palette-Surface
- ausschließlich Gefahrstufe `none`
- aktive Workspace-Grenzen über `VendooNavigation.currentView`
- korrekte Asset-Ladereihenfolge
- Modul-Refresh-Delegation
- Modulfilter-Delegation
- Artikelansicht-Delegation
- fokussierten Artikelkontext
- fokussierten Medienkontext
- Galeriedichte und Medienfilter-Reset
- Ready-Event
- Ausschluss von Netzwerk-, Polling-, `innerHTML`-, Bestätigungs- und direkter Fachlogik
- Einbindung in `verify:all`
Lokal bestanden:
```text
node --check public/safe-action-adapters.js
node --check tools/verify-safe-action-adapters-1.44.0.mjs
Safe Action Adapter Gate bestanden.
```
## Daten- und Releasegrenzen
- keine bestehenden Routen, APIs, Feld-IDs oder Modul-IDs umbenannt
- keine bestehenden Handler ersetzt
- keine persistenten Nutzdaten verändert
- kein Merge
- kein Deployment
- kein Release
@@ -0,0 +1,97 @@
# Vendoo 1.44.0 Slice 19 CI-Diagnosevertrag
## Zweck
Der vollständige Vendoo-Releasevertrag prüft das Release-Manifest bewusst sehr früh. Solange das Manifest nicht auf den aktuellen Branch-Head synchronisiert ist, können nachgelagerte UX-Gates in diesem Workflow nicht eindeutig ausgewertet werden.
Slice 19 führt deshalb einen unabhängigen, minimalen Plattformvertrag ein. Dieser ersetzt den vollständigen Releasevertrag nicht, sondern trennt den fachlichen Zustand von Navigation API und Safe Action Adapters vom Paket- und Manifestzustand.
## Workflow
Datei:
```text
.github/workflows/ux-platform-contract.yml
```
Matrix:
- `ubuntu-latest`
- `windows-latest`
## Unabhängigkeit
Der Workflow verwendet bewusst:
- keine `actions/checkout`-Action
- keine `actions/setup-node`-Action
- kein `npm ci`
- keinen Dependency-Cache
- keine Drittanbieter-Action
Der exakte Workflow-Commit wird ausschließlich mit dem auf dem GitHub-Runner vorhandenen `git` ausgecheckt:
```text
git init .
git remote add origin <Repository-URL>
git fetch --depth=1 origin <GITHUB_SHA>
git checkout --detach FETCH_HEAD
```
Damit hängen die beiden UX-Gates weder von NPM-Abhängigkeiten noch von Marketplace-Actions ab.
## Ausgeführte Gates
Der Runner `tools/run-ux-platform-gates.mjs` startet mit dem vorhandenen Node-Prozess:
1. `tools/verify-navigation-api-1.44.0.mjs`
2. `tools/verify-safe-action-adapters-1.44.0.mjs`
Beide Verifier verwenden ausschließlich Node-Coremodule.
## Diagnoseausgabe
Der Runner protokolliert:
- Betriebssystem
- Node-Version
- Architektur
- geprüften Commit
- Standardausgabe jedes Gates
- Fehlerausgabe jedes Gates
- Exitcode jedes Gates
- Gesamtergebnis
Die Ausgabe wird zusätzlich unter folgendem Pfad erzeugt:
```text
artifacts/ux-platform-gates.log
```
Der abschließende Workflow-Step gibt vorhandene Diagnoseprotokolle unabhängig vom Gate-Ergebnis erneut in das Jobprotokoll aus.
## Abgrenzung
Ein grüner UX-Plattformvertrag bestätigt ausschließlich:
- öffentliche Navigation API
- Navigation-Lifecycle
- Workspace-Grenzen
- 21 Safe Action Adapters
- bestehende Handlerdelegation
- Linux-/Windows-Kompatibilität dieser Verifier
Er bestätigt nicht automatisch:
- synchrones Release-Manifest
- vollständigen Windows-Releasevertrag
- Docker-Build
- Updater-Paket
- Deployment- oder Releasefreigabe
## Releasegrenzen
- kein Merge
- kein Deployment
- kein Release
- keine Abschwächung des vollständigen Releasevertrags
@@ -0,0 +1,172 @@
# Vendoo 1.44.0 UX Slice 19: Safe Action Expansion
## Referenz
- Branch: `release/1.44.0-ux-foundation-production-update-v1`
- Ausgangs-Head: `dd9596ef857b20c63a8773837a8853d7a657e4a9`
- App-Ziel: Vendoo `1.44.0`
- Updater: `3.4.0`
## Ziel
Slice 19 vervollständigt die erste sichere Action-Migrationsstufe. Die neun Adapter aus Slice 18 bleiben unverändert verfügbar und werden um zwölf ausschließlich darstellende oder filternde Actions ergänzt.
Es entstehen keine neuen Fachaktionen. Jeder neue Eintrag delegiert weiterhin ausschließlich an einen bereits vorhandenen Vendoo-Button.
## Vollständiger Action-Umfang
Die Safe Action Adapter registrieren jetzt exakt 21 Actions.
### Module
- Module neu laden
- alle Module anzeigen
- aktive Module anzeigen
- deaktivierte Module anzeigen
- installierte externe Module anzeigen
- Module mit Problemen anzeigen
### Browser-Extensions
- alle Extensions anzeigen
- bereite Extensions anzeigen
- verbundene Extensions anzeigen
- Extensions mit Handlungsbedarf anzeigen
### Artikel
- aktuellen Artikel öffnen
- Kartenansicht aktivieren
- Listenansicht aktivieren
- Artikelfilter zurücksetzen
### Medien
- aktuelles Bild groß öffnen
- kompakte Galeriedichte aktivieren
- normale Galeriedichte aktivieren
- große Galeriedichte aktivieren
- Rasteransicht aktivieren
- Listenansicht aktivieren
- Medienfilter zurücksetzen
## Deklarativer Adapterkern
Wiederkehrende Button-Delegationen werden jetzt über `buttonAction(...)` beschrieben. Der Helper erzeugt ausschließlich:
- `enabled`, indem der vorhandene Zielbutton geprüft wird
- `handler`, indem exakt dieser Zielbutton per `click()` ausgelöst wird
Dadurch bleiben Action-Metadaten und technische Delegation zentral, einheitlich und leichter prüfbar. Artikel- und Medienkontextaktionen behalten ihre spezialisierten Handler.
## Öffentliche Navigation API
Bei der Härtung wurde festgestellt, dass die Safe Action Adapter bereits optional `window.VendooNavigation.currentView` berücksichtigten, die globale Navigation dieses API aber noch nicht veröffentlichte.
`public/navigation-api.js` schließt diese Vertragslücke und stellt jetzt ein schreibgeschütztes API bereit:
- `window.VendooNavigation.currentView`
- dynamischer Getter für die tatsächlich aktive Vendoo-Ansicht
- `window.VendooNavigation.canOpen(view)`
- prüft vorhandenen, sichtbaren und aktivierbaren Navigationseintrag
- `window.VendooNavigation.open(view)`
- delegiert ausschließlich per `click()` an den vorhandenen Navigationseintrag
Das API führt keine eigene History-, Routing- oder URL-Logik ein. Die bestehende globale Navigation bleibt alleinige Quelle dieser Abläufe.
Zusätzlich werden folgende Lifecycle-Events veröffentlicht:
- `vendoo:navigation-api-ready`
- `vendoo:navigation-changed`
Der Navigation-Observer reagiert ausschließlich auf Änderungen von `class`, `hidden`, `aria-hidden` und `aria-disabled` und wird bei `beforeunload` sauber getrennt.
## Workspace-Grenzen
Sichtbarkeit wird gegen `window.VendooNavigation.currentView` und den zugehörigen DOM-Bereich geprüft:
- Modulaktionen: `admin`
- Extension-Aktionen: `extensions`
- Artikelaktionen: `history`
- Medienaktionen: `media-library`
Actions eines inaktiven Arbeitsbereichs erscheinen nicht in der Command Palette.
## Asset-Ladereihenfolge
Die Plattformassets werden jetzt in folgender Reihenfolge geladen:
1. Command Platform
2. Universal Action Registry
3. Global Navigation
4. Navigation API
5. Safe Action Adapters
Damit ist der öffentliche Navigationsvertrag vor der ersten Action-Auswertung verfügbar.
## Sicherheitsgrenzen
Alle 21 Actions erfüllen denselben Vertrag:
- ausschließlich `danger: none`
- ausschließlich Surface `command-palette`
- keine Veröffentlichung
- keine Löschung
- keine Installation oder Deinstallation
- keine Aktivierung oder Deaktivierung eines Moduls
- keine Status- oder Datenänderung
- keine Speicherung
- keine Datenbankänderung
- keine Migration
- kein eigener API-Aufruf
- kein Polling
- kein `window.confirm`
- kein `innerHTML`
Auch die Navigation API führt keine Netzwerkaufrufe, externe Navigation oder eigene Browser-History-Manipulation aus.
## Abnahme-Gates
Das vorhandene Gate `npm run verify:safe-action-adapters` wurde auf den vollständigen 21-Action-Vertrag erweitert.
Es prüft jetzt zusätzlich:
- exakt 21 eindeutige Action-IDs
- zwölf neue Expansion-Actions
- alle vier Workspace-Grenzen
- Modulfilter für aktiv, deaktiviert und installiert
- Extension-Filter für bereit, verbunden und Handlungsbedarf
- Artikelfilter-Reset
- kompakte und große Galeriedichte
- Medien-Raster- und Listenansicht
- Ready-Event mit
- `count: 21`
- `baselineCount: 9`
- `expansionCount: 12`
Neues Navigation-API-Gate:
```bash
npm run verify:navigation-api
```
Es prüft:
- schreibgeschütztes öffentliches API
- dynamischen `currentView`-Getter
- `canOpen` für sichtbare, versteckte und unbekannte Ansichten
- kontrollierte Delegation von `open()`
- vollständige Lifecycle-Events
- Observer-Aufbau und Cleanup
- Asset-Ladereihenfolge
- Ausschluss eigener History-, Netzwerk-, Polling- und externer Navigationslogik
- Einbindung in `verify:all`
## Releasegrenzen
- keine bestehenden Handler ersetzt
- keine bestehenden Routen, APIs, Feld-IDs oder Modul-IDs geändert
- keine persistenten Nutzdaten verändert
- kein Merge
- kein Deployment
- kein Release
+20 -1
View File
@@ -22,7 +22,26 @@
"verify:git-staging": "node tools/verify-git-staging.mjs",
"generate:tokens": "node tools/generate-design-tokens.mjs",
"verify:architecture": "node tools/verify-platform-architecture-1.41.2.mjs",
"verify:all": "npm run verify:git && npm run verify:git-regression && npm run verify:git-staging && npm run verify:release-manifest && npm run verify:ignore-boundaries && npm run check && npm run verify:powershell-static && npm run verify:source-boundaries && npm run verify:esm-exports && npm run verify:flux-loop && npm run verify:capabilities && npm run verify:coolify && npm run verify:initial-modules && npm run verify:architecture && npm run verify:themes && npm run verify:security && npm run verify:identity && npm run verify:invitations && npm run verify:sqlite-cleanup && npm run verify:catalog && npm run verify:modules && npm run verify:production-operations && npm run verify:updater-start-chain && npm run verify:updater-ci-lifecycle && npm run verify:one-click-update && npm run verify:module-navigation && npm run verify:updater-ui && npm run verify:updater-hotfix && npm run verify:deployment && npm run verify:config && npm run verify:installer-shell && npm run verify:db",
"verify:ux-foundation": "node tools/verify-ux-foundation-1.44.0.mjs",
"verify:generator-workflow": "node tools/verify-generator-workflow-1.44.0.mjs",
"verify:article-overview": "node tools/verify-article-overview-ux-1.44.0.mjs",
"verify:publish-center": "node tools/verify-publish-center-ux-1.44.0.mjs",
"verify:media-library": "node tools/verify-media-library-ux-1.44.0.mjs",
"verify:image-factory": "node tools/verify-image-factory-ux-1.44.0.mjs",
"verify:flux-studio-ux": "node tools/verify-flux-studio-ux-1.44.0.mjs",
"verify:quality-center-ux": "node tools/verify-quality-center-ux-1.44.0.mjs",
"verify:inventory-workspace": "node tools/verify-inventory-workspace-ux-1.44.0.mjs",
"verify:template-workspace": "node tools/verify-template-workspace-ux-1.44.0.mjs",
"verify:today-workspace": "node tools/verify-today-workspace-ux-1.44.0.mjs",
"verify:settings-control-center": "node tools/verify-settings-control-center-ux-1.44.0.mjs",
"verify:module-extension-workspace": "node tools/verify-module-extension-workspace-ux-1.44.0.mjs",
"verify:global-navigation": "node tools/verify-global-navigation-ux-1.44.0.mjs",
"verify:command-platform": "node tools/verify-command-platform-ux-1.44.0.mjs",
"verify:command-palette-registry": "node tools/verify-command-palette-registry-1.44.0.mjs",
"verify:universal-actions": "node tools/verify-universal-action-registry-1.44.0.mjs",
"verify:navigation-api": "node tools/verify-navigation-api-1.44.0.mjs",
"verify:safe-action-adapters": "node tools/verify-safe-action-adapters-1.44.0.mjs",
"verify:all": "npm run verify:git && npm run verify:git-regression && npm run verify:git-staging && npm run verify:release-manifest && npm run verify:ignore-boundaries && npm run check && npm run verify:powershell-static && npm run verify:source-boundaries && npm run verify:esm-exports && npm run verify:flux-loop && npm run verify:capabilities && npm run verify:coolify && npm run verify:initial-modules && npm run verify:architecture && npm run verify:ux-foundation && npm run verify:generator-workflow && npm run verify:article-overview && npm run verify:publish-center && npm run verify:media-library && npm run verify:image-factory && npm run verify:flux-studio-ux && npm run verify:quality-center-ux && npm run verify:inventory-workspace && npm run verify:template-workspace && npm run verify:today-workspace && npm run verify:settings-control-center && npm run verify:module-extension-workspace && npm run verify:global-navigation && npm run verify:command-platform && npm run verify:command-palette-registry && npm run verify:universal-actions && npm run verify:navigation-api && npm run verify:safe-action-adapters && npm run verify:themes && npm run verify:security && npm run verify:identity && npm run verify:invitations && npm run verify:sqlite-cleanup && npm run verify:catalog && npm run verify:modules && npm run verify:production-operations && npm run verify:updater-start-chain && npm run verify:updater-ci-lifecycle && npm run verify:one-click-update && npm run verify:module-navigation && npm run verify:updater-ui && npm run verify:updater-hotfix && npm run verify:deployment && npm run verify:config && npm run verify:installer-shell && npm run verify:db",
"verify:themes": "node tools/verify-theme-manager-1.41.2.mjs",
"verify:security": "node tools/verify-security-foundation-1.41.2.mjs",
"verify:identity": "node tools/verify-identity-settings-1.41.2.mjs && node --no-warnings tools/verify-identity-db-1.40.0.mjs",
File diff suppressed because one or more lines are too long
+617
View File
@@ -0,0 +1,617 @@
(() => {
'use strict';
if (document.documentElement.dataset.uxActionRegistryReady === '1') return;
document.documentElement.dataset.uxActionRegistryReady = '1';
const registry = new Map();
const listeners = new Set();
const commandDisposers = new Map();
const running = new Map();
const mountedSurfaces = new Set();
const elementContexts = new WeakMap();
const DANGER_LEVELS = new Set(['none', 'caution', 'destructive']);
const CONCURRENCY_MODES = new Set(['single', 'parallel']);
let confirmationProvider = null;
let auditSink = null;
let domObserver = null;
const normalizeStringList = value => Array.isArray(value)
? [...new Set(value.map(item => String(item || '').trim()).filter(Boolean))]
: [];
const normalizeNumber = (value, fallback = 0) => {
const number = Number(value);
return Number.isFinite(number) ? number : fallback;
};
function normalizeSelection(value) {
if (!value || typeof value !== 'object') return Object.freeze({ min: 0, max: null });
const min = Math.max(0, Math.floor(normalizeNumber(value.min, 0)));
const maxCandidate = value.max === null || value.max === undefined ? null : Math.max(min, Math.floor(normalizeNumber(value.max, min)));
return Object.freeze({ min, max: maxCandidate });
}
function normalizeConfirmation(value, action) {
if (value === false && action.danger !== 'destructive') return null;
const source = value && typeof value === 'object' ? value : {};
if (value !== true && !source.title && !source.message && action.danger !== 'destructive') return null;
return Object.freeze({
title: source.title ?? (action.danger === 'destructive' ? `${action.title} bestätigen` : action.title),
message: source.message ?? (action.danger === 'destructive'
? 'Diese Aktion kann Daten oder einen Betriebszustand dauerhaft verändern. Prüfe den Kontext und bestätige bewusst.'
: 'Möchtest du diese Aktion ausführen?'),
confirmLabel: source.confirmLabel ?? (action.danger === 'destructive' ? 'Verbindlich ausführen' : 'Ausführen'),
cancelLabel: source.cancelLabel ?? 'Abbrechen',
phrase: source.phrase ?? '',
});
}
function normalizeAction(input) {
if (!input || typeof input !== 'object') throw new TypeError('Action muss ein Objekt sein.');
const id = String(input.id || '').trim();
const title = String(input.title || '').trim();
if (!id || !title) throw new TypeError('Action benötigt id und title.');
if (typeof input.handler !== 'function') throw new TypeError(`Action ${id} benötigt einen handler.`);
const danger = DANGER_LEVELS.has(input.danger) ? input.danger : (input.danger === true ? 'destructive' : 'none');
const concurrency = CONCURRENCY_MODES.has(input.concurrency) ? input.concurrency : 'single';
const base = {
id,
title,
description: String(input.description || ''),
icon: String(input.icon || '•'),
group: String(input.group || 'Allgemein'),
aliases: normalizeStringList(input.aliases),
keywords: normalizeStringList(input.keywords),
surfaces: normalizeStringList(input.surfaces),
shortcut: String(input.shortcut || '').trim(),
permission: input.permission ?? null,
danger,
priority: normalizeNumber(input.priority, 0),
visible: input.visible ?? null,
enabled: input.enabled ?? null,
disabledReason: String(input.disabledReason || ''),
contexts: normalizeStringList(input.contexts),
selection: normalizeSelection(input.selection),
concurrency,
allowInInput: input.allowInInput === true,
handler: input.handler,
rollback: typeof input.rollback === 'function' ? input.rollback : null,
commandId: input.commandId ? String(input.commandId) : `action:${id}`,
};
return Object.freeze({ ...base, confirm: normalizeConfirmation(input.confirm, base) });
}
const emit = (type, detail = {}) => {
const payload = Object.freeze({ ...detail });
window.dispatchEvent(new CustomEvent(`vendoo:${type}`, { detail: payload }));
listeners.forEach(listener => {
try { listener({ type, detail: payload }); } catch {}
});
if (typeof auditSink === 'function') {
try { auditSink({ type, detail: payload }); } catch {}
}
};
function evaluateRule(rule, context, fallback) {
if (typeof rule === 'function') {
try { return Boolean(rule(context)); } catch { return false; }
}
if (typeof rule === 'boolean') return rule;
return fallback;
}
function evaluatePermission(permission, context) {
if (permission === null || permission === undefined || permission === '') return true;
if (typeof permission === 'function') {
try { return Boolean(permission(context)); } catch { return false; }
}
if (typeof permission === 'boolean') return permission;
const resolver = context?.canPermission || window.VendooPermissions?.can;
if (typeof resolver !== 'function') return false;
try {
if (Array.isArray(permission)) return permission.every(value => resolver.call(window.VendooPermissions, value, context));
return Boolean(resolver.call(window.VendooPermissions, permission, context));
} catch {
return false;
}
}
function selectionCount(context) {
if (Number.isFinite(Number(context?.selectionCount))) return Math.max(0, Number(context.selectionCount));
if (Array.isArray(context?.selection)) return context.selection.length;
if (context?.selection instanceof Set || context?.selection instanceof Map) return context.selection.size;
return 0;
}
function matchesContext(action, context) {
if (!action.contexts.length) return true;
const supplied = new Set([
...normalizeStringList(context?.contexts),
...(context?.scope ? [String(context.scope)] : []),
...(context?.view ? [String(context.view)] : []),
]);
return action.contexts.some(value => supplied.has(value));
}
function resolveAction(actionOrId, context = {}) {
const action = typeof actionOrId === 'string' ? registry.get(actionOrId) : actionOrId;
if (!action) return null;
const contextMatches = matchesContext(action, context);
const permissionGranted = evaluatePermission(action.permission, context);
const visible = contextMatches && permissionGranted && evaluateRule(action.visible, context, true);
const count = selectionCount(context);
const selectionValid = count >= action.selection.min && (action.selection.max === null || count <= action.selection.max);
const isRunning = running.has(action.id);
const enabled = visible
&& selectionValid
&& !(action.concurrency === 'single' && isRunning)
&& evaluateRule(action.enabled, context, true);
let disabledReason = '';
if (!enabled) {
if (!visible) disabledReason = action.disabledReason || 'Diese Aktion ist im aktuellen Kontext nicht verfügbar.';
else if (!selectionValid) {
if (count < action.selection.min) disabledReason = action.disabledReason || `Mindestens ${action.selection.min} Auswahl erforderlich.`;
else disabledReason = action.disabledReason || `Höchstens ${action.selection.max} Elemente können gleichzeitig verarbeitet werden.`;
} else if (isRunning) disabledReason = 'Diese Aktion wird bereits ausgeführt.';
else disabledReason = action.disabledReason || 'Diese Aktion ist derzeit nicht verfügbar.';
}
return Object.freeze({ visible, enabled, disabledReason, running: isRunning, selectionCount: count });
}
function list({ surface = null, group = null, context = {}, includeDisabled = true, includeHidden = false } = {}) {
return [...registry.values()]
.map(action => Object.freeze({ ...action, ...resolveAction(action, context) }))
.filter(action => (!surface || action.surfaces.includes(surface))
&& (!group || action.group === group)
&& (includeHidden || action.visible)
&& (includeDisabled || action.enabled))
.sort((left, right) => right.priority - left.priority
|| left.group.localeCompare(right.group, 'de-DE', { sensitivity: 'base' })
|| left.title.localeCompare(right.title, 'de-DE', { sensitivity: 'base' }));
}
function syncCommandAdapter(action, { preserveExisting = false } = {}) {
if (!preserveExisting) {
commandDisposers.get(action.id)?.();
commandDisposers.delete(action.id);
}
if (!action.surfaces.includes('command-palette') || !window.VendooCommands?.register) return;
const dispose = window.VendooCommands.register({
id: action.commandId,
title: action.title,
description: action.description,
icon: action.icon,
group: action.group,
aliases: action.aliases,
keywords: action.keywords,
shortcut: action.shortcut,
kind: 'action',
visible: context => Boolean(resolveAction(action, context)?.visible),
enabled: context => Boolean(resolveAction(action, context)?.enabled),
disabledReason: action.disabledReason || 'Diese Aktion ist im aktuellen Kontext nicht verfügbar.',
execute: context => {
void execute(action.id, { ...context, source: context?.source || 'command-palette' });
return true;
},
});
commandDisposers.set(action.id, dispose);
}
function register(input, { replace = false } = {}) {
const action = normalizeAction(input);
if (registry.has(action.id) && !replace) throw new Error(`Action-ID ist bereits registriert: ${action.id}`);
if (registry.has(action.id)) unregister(action.id);
registry.set(action.id, action);
syncCommandAdapter(action);
emit('actions-changed', { actionId: action.id, operation: 'register' });
scheduleRefresh();
return () => unregister(action.id);
}
function registerMany(actions, options = {}) {
if (!Array.isArray(actions)) throw new TypeError('registerMany erwartet ein Array.');
const disposers = actions.map(action => register(action, options));
return () => disposers.reverse().forEach(dispose => dispose());
}
function unregister(id) {
const actionId = String(id);
const removed = registry.delete(actionId);
commandDisposers.get(actionId)?.();
commandDisposers.delete(actionId);
if (removed) {
emit('actions-changed', { actionId, operation: 'unregister' });
scheduleRefresh();
}
return removed;
}
function resolveText(value, context, fallback = '') {
if (typeof value === 'function') {
try { return String(value(context) ?? fallback); } catch { return fallback; }
}
return String(value ?? fallback);
}
function createElement(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function requestDefaultConfirmation(action, context) {
if (!document.body || typeof document.createElement !== 'function') return Promise.resolve(false);
const config = action.confirm || normalizeConfirmation(true, action);
return new Promise(resolve => {
const overlay = createElement('div', 'vd-action-confirm-overlay');
overlay.setAttribute('role', 'presentation');
const dialog = createElement('section', 'vd-action-confirm-dialog');
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('aria-modal', 'true');
dialog.setAttribute('aria-labelledby', 'vd-action-confirm-title');
dialog.setAttribute('aria-describedby', 'vd-action-confirm-message');
dialog.dataset.tone = action.danger;
const header = createElement('header', 'vd-action-confirm-header');
const marker = createElement('span', 'vd-action-confirm-marker', action.danger === 'destructive' ? '!' : '?');
marker.setAttribute('aria-hidden', 'true');
const heading = createElement('div', 'vd-action-confirm-heading');
const title = createElement('h2', '', resolveText(config.title, context, `${action.title} bestätigen`));
title.id = 'vd-action-confirm-title';
const risk = createElement('span', 'vd-action-confirm-risk', action.danger === 'destructive' ? 'Destruktive Aktion' : 'Bestätigung erforderlich');
heading.append(risk, title);
header.append(marker, heading);
const message = createElement('p', 'vd-action-confirm-message', resolveText(config.message, context));
message.id = 'vd-action-confirm-message';
const phrase = resolveText(config.phrase, context).trim();
let phraseInput = null;
const body = createElement('div', 'vd-action-confirm-body');
body.append(message);
if (phrase) {
const label = createElement('label', 'vd-action-confirm-phrase');
label.append(createElement('span', '', `Zur Bestätigung „${phrase}“ eingeben`));
phraseInput = createElement('input');
phraseInput.type = 'text';
phraseInput.autocomplete = 'off';
phraseInput.spellcheck = false;
label.append(phraseInput);
body.append(label);
}
const footer = createElement('footer', 'vd-action-confirm-footer');
const cancel = createElement('button', 'vd-action-confirm-cancel', resolveText(config.cancelLabel, context, 'Abbrechen'));
cancel.type = 'button';
const confirm = createElement('button', 'vd-action-confirm-submit', resolveText(config.confirmLabel, context, 'Ausführen'));
confirm.type = 'button';
if (phraseInput) confirm.disabled = true;
footer.append(cancel, confirm);
dialog.append(header, body, footer);
overlay.append(dialog);
document.body.append(overlay);
document.body.classList.add('vd-action-confirm-open');
const previousFocus = document.activeElement;
let settled = false;
const finish = value => {
if (settled) return;
settled = true;
document.removeEventListener('keydown', onKeydown, true);
overlay.remove();
document.body.classList.remove('vd-action-confirm-open');
if (previousFocus instanceof HTMLElement && previousFocus.isConnected) previousFocus.focus({ preventScroll: true });
resolve(value);
};
const focusables = () => [...dialog.querySelectorAll('button:not(:disabled), input:not(:disabled), [tabindex]:not([tabindex="-1"])')];
const onKeydown = event => {
if (event.key === 'Escape') {
event.preventDefault();
finish(false);
return;
}
if (event.key !== 'Tab') return;
const items = focusables();
if (!items.length) return;
const first = items[0];
const last = items[items.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
};
document.addEventListener('keydown', onKeydown, true);
cancel.addEventListener('click', () => finish(false));
confirm.addEventListener('click', () => finish(true));
overlay.addEventListener('mousedown', event => { if (event.target === overlay) finish(false); });
phraseInput?.addEventListener('input', () => { confirm.disabled = phraseInput.value.trim() !== phrase; });
requestAnimationFrame(() => (phraseInput || cancel).focus({ preventScroll: true }));
});
}
async function requestConfirmation(action, context) {
if (!action.confirm && action.danger !== 'destructive') return true;
const provider = confirmationProvider || requestDefaultConfirmation;
try { return Boolean(await provider(action, context)); } catch { return false; }
}
async function execute(id, context = {}) {
const action = registry.get(String(id));
if (!action) return false;
const status = resolveAction(action, context);
if (!status?.visible || !status.enabled) {
emit('action-blocked', { actionId: action.id, source: context?.source || 'api', reason: status?.disabledReason || 'Aktion nicht verfügbar.' });
return false;
}
if (!(await requestConfirmation(action, context))) {
emit('action-cancelled', { actionId: action.id, source: context?.source || 'api', reason: 'confirmation-declined' });
return false;
}
const startedAt = Date.now();
if (action.concurrency === 'single') running.set(action.id, startedAt);
emit('action-executing', { actionId: action.id, source: context?.source || 'api', danger: action.danger });
scheduleRefresh();
try {
const result = await action.handler(Object.freeze({ ...context, actionId: action.id }));
if (result === false) {
emit('action-cancelled', { actionId: action.id, source: context?.source || 'api', reason: 'handler-declined', durationMs: Date.now() - startedAt });
return false;
}
emit('action-executed', { actionId: action.id, source: context?.source || 'api', durationMs: Date.now() - startedAt });
return true;
} catch (error) {
let rolledBack = false;
if (action.rollback) {
try {
await action.rollback(Object.freeze({ ...context, actionId: action.id, error }));
rolledBack = true;
emit('action-rolled-back', { actionId: action.id, source: context?.source || 'api' });
} catch (rollbackError) {
emit('action-rollback-failed', {
actionId: action.id,
source: context?.source || 'api',
message: rollbackError?.message || String(rollbackError),
});
}
}
emit('action-failed', {
actionId: action.id,
source: context?.source || 'api',
message: error?.message || String(error),
rolledBack,
durationMs: Date.now() - startedAt,
});
return false;
} finally {
if (action.concurrency === 'single') running.delete(action.id);
scheduleRefresh();
}
}
function parseElementContext(element) {
const stored = elementContexts.get(element) || {};
let declared = {};
const raw = element.dataset.vendooActionContext;
if (raw) {
try {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) declared = parsed;
} catch {}
}
const scope = element.closest?.('[data-vendoo-action-scope]')?.dataset.vendooActionScope;
return {
...stored,
...declared,
scope: declared.scope || stored.scope || scope || window.VendooNavigation?.currentView || 'global',
view: declared.view || stored.view || window.VendooNavigation?.currentView || null,
source: declared.source || stored.source || element.dataset.vendooActionSource || 'dom',
trigger: element,
};
}
function syncElement(element) {
const actionId = element?.dataset?.vendooAction;
if (!actionId) return;
const action = registry.get(actionId);
const status = action ? resolveAction(action, parseElementContext(element)) : null;
const enabled = Boolean(status?.enabled);
element.setAttribute('aria-disabled', enabled ? 'false' : 'true');
if ('disabled' in element) element.disabled = !enabled;
element.classList.toggle('vd-action-running', Boolean(status?.running));
element.classList.toggle('vd-action-destructive', action?.danger === 'destructive');
element.classList.toggle('vd-action-caution', action?.danger === 'caution');
if (!element.dataset.vendooActionOriginalTitle) element.dataset.vendooActionOriginalTitle = element.getAttribute('title') || '';
const originalTitle = element.dataset.vendooActionOriginalTitle;
element.title = enabled ? originalTitle : (status?.disabledReason || 'Aktion nicht verfügbar.');
}
function refreshDom(root = document) {
root.querySelectorAll?.('[data-vendoo-action]').forEach(syncElement);
if (root.matches?.('[data-vendoo-action]')) syncElement(root);
}
function scheduleRefresh() {
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => refresh());
else refresh();
}
function refresh(actionId = null) {
if (actionId && registry.has(actionId)) syncCommandAdapter(registry.get(actionId), { preserveExisting: true });
else if (!actionId) registry.forEach(action => syncCommandAdapter(action, { preserveExisting: true }));
refreshDom();
mountedSurfaces.forEach(entry => {
try { entry.render(); } catch {}
});
emit('action-state-changed', { actionId });
}
function createActionButton(action, context, variant) {
const button = createElement('button', 'vd-action-button');
button.type = 'button';
button.dataset.vendooAction = action.id;
button.dataset.actionVariant = variant;
button.dataset.actionDanger = action.danger;
elementContexts.set(button, context);
const icon = createElement('span', 'vd-action-button-icon', action.icon);
icon.setAttribute('aria-hidden', 'true');
const copy = createElement('span', 'vd-action-button-copy');
copy.append(createElement('strong', '', action.title));
if (action.description && variant !== 'toolbar') copy.append(createElement('small', '', action.description));
button.append(icon, copy);
if (action.shortcut && variant !== 'sheet') button.append(createElement('kbd', '', action.shortcut));
syncElement(button);
return button;
}
function renderSurface(container, { surface, context = {}, variant = 'toolbar', includeDisabled = true, emptyText = '' } = {}) {
if (!container || typeof container.replaceChildren !== 'function') throw new TypeError('renderSurface benötigt einen DOM-Container.');
if (!surface) throw new TypeError('renderSurface benötigt einen surface-Namen.');
const actions = list({ surface, context, includeDisabled });
container.classList.add('vd-action-surface');
container.dataset.actionSurface = surface;
container.dataset.actionVariant = variant;
container.replaceChildren();
if (!actions.length && emptyText) container.append(createElement('span', 'vd-action-surface-empty', emptyText));
else actions.forEach(action => container.append(createActionButton(action, context, variant)));
return actions;
}
function mountSurface(container, options = {}) {
const contextProvider = typeof options.context === 'function' ? options.context : () => (options.context || {});
const entry = {
container,
render: () => renderSurface(container, { ...options, context: contextProvider() }),
};
mountedSurfaces.add(entry);
entry.render();
return () => {
mountedSurfaces.delete(entry);
container.replaceChildren();
container.classList.remove('vd-action-surface');
delete container.dataset.actionSurface;
delete container.dataset.actionVariant;
};
}
function normalizeShortcut(value) {
const aliases = { control: 'Ctrl', ctrl: 'Ctrl', cmd: 'Meta', command: 'Meta', meta: 'Meta', option: 'Alt', alt: 'Alt', shift: 'Shift' };
const parts = String(value || '').split('+').map(part => part.trim()).filter(Boolean);
const modifiers = [];
let key = '';
parts.forEach(part => {
const normalized = aliases[part.toLocaleLowerCase('en-US')];
if (normalized) modifiers.push(normalized);
else key = part.length === 1 ? part.toLocaleUpperCase('en-US') : part;
});
return [...new Set(modifiers)].sort((a, b) => ['Ctrl', 'Alt', 'Shift', 'Meta'].indexOf(a) - ['Ctrl', 'Alt', 'Shift', 'Meta'].indexOf(b)).concat(key || []).join('+');
}
function eventShortcut(event) {
const parts = [];
if (event.ctrlKey) parts.push('Ctrl');
if (event.altKey) parts.push('Alt');
if (event.shiftKey) parts.push('Shift');
if (event.metaKey) parts.push('Meta');
const key = event.key.length === 1 ? event.key.toLocaleUpperCase('en-US') : event.key;
if (!['Control', 'Alt', 'Shift', 'Meta'].includes(key)) parts.push(key);
return normalizeShortcut(parts.join('+'));
}
function isEditableTarget(target) {
return Boolean(target?.closest?.('input, textarea, select, [contenteditable="true"], [role="textbox"]'));
}
function handleShortcut(event) {
if (event.defaultPrevented) return;
const shortcut = eventShortcut(event);
if (!shortcut) return;
const context = { source: 'shortcut', event, scope: window.VendooNavigation?.currentView || 'global', view: window.VendooNavigation?.currentView || null };
const candidates = list({ surface: 'shortcut', context, includeDisabled: false })
.filter(action => normalizeShortcut(action.shortcut) === shortcut)
.filter(action => action.allowInInput || !isEditableTarget(event.target));
if (!candidates.length) return;
const highest = candidates[0].priority;
const winners = candidates.filter(action => action.priority === highest);
event.preventDefault();
if (winners.length !== 1) {
emit('action-shortcut-conflict', { shortcut, actionIds: winners.map(action => action.id) });
return;
}
void execute(winners[0].id, context);
}
function bindDom() {
document.addEventListener('click', event => {
const trigger = event.target?.closest?.('[data-vendoo-action]');
if (!trigger) return;
const actionId = trigger.dataset.vendooAction;
const status = resolveAction(actionId, parseElementContext(trigger));
if (!status?.enabled) {
event.preventDefault();
emit('action-blocked', { actionId, source: 'dom', reason: status?.disabledReason || 'Aktion nicht verfügbar.' });
return;
}
event.preventDefault();
void execute(actionId, parseElementContext(trigger));
});
document.addEventListener('keydown', handleShortcut);
['vendoo:modules-changed', 'vendoo:navigation-changed', 'vendoo:selection-changed', 'vendoo:permissions-changed']
.forEach(type => window.addEventListener(type, () => scheduleRefresh()));
if ('MutationObserver' in window && document.body) {
domObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) refreshDom(node);
}));
});
domObserver.observe(document.body, { childList: true, subtree: true });
window.addEventListener('beforeunload', () => domObserver?.disconnect(), { once: true });
}
refreshDom();
}
window.VendooActions = Object.freeze({
register,
registerMany,
unregister,
list,
get: id => registry.get(String(id)) || null,
resolve: (id, context = {}) => resolveAction(String(id), context),
canExecute: (id, context = {}) => Boolean(resolveAction(String(id), context)?.enabled),
execute,
refresh,
renderSurface,
mountSurface,
normalizeShortcut,
setConfirmationProvider(provider) {
if (provider !== null && typeof provider !== 'function') throw new TypeError('Confirmation Provider muss eine Funktion oder null sein.');
confirmationProvider = provider;
},
setAuditSink(sink) {
if (sink !== null && typeof sink !== 'function') throw new TypeError('Audit Sink muss eine Funktion oder null sein.');
auditSink = sink;
},
get running() { return [...running.keys()]; },
subscribe(listener) {
if (typeof listener !== 'function') throw new TypeError('Listener muss eine Funktion sein.');
listeners.add(listener);
return () => listeners.delete(listener);
},
});
bindDom();
emit('actions-ready', { count: registry.size });
})();
+497
View File
@@ -0,0 +1,497 @@
/* Vendoo Article Overview UX 1.44.0
* Aufgabenorientierte Filter, adaptive Kartenansicht und reduzierte Mehrfachaktionen.
*/
#history.ux-article-overview-ready {
--ux-article-card-min: 340px;
}
.ux-article-toolbar {
align-items: center;
background: var(--vd-surface-raised);
border: 1px solid var(--vd-line);
border-radius: var(--vd-radius-md);
display: grid;
gap: 14px;
grid-template-columns: minmax(180px, .7fr) minmax(320px, 1.4fr) auto;
margin-bottom: 12px;
padding: 14px 15px;
}
.ux-article-summary strong,
.ux-article-summary span {
display: block;
}
.ux-article-summary strong {
color: var(--vd-ink);
font-size: 13px;
letter-spacing: -.01em;
}
.ux-article-summary span {
color: var(--vd-ink-muted);
font-size: 9px;
margin-top: 3px;
}
.ux-status-filters,
.ux-article-view-switch {
align-items: center;
display: flex;
gap: 5px;
}
.ux-status-filters {
flex-wrap: wrap;
}
.ux-status-filter,
.ux-view-choice {
align-items: center;
background: var(--vd-surface);
border: 1px solid var(--vd-line);
border-radius: 999px;
color: var(--vd-ink-secondary);
cursor: pointer;
display: inline-flex;
font: inherit;
font-size: 10px;
font-weight: 680;
justify-content: center;
min-height: 32px;
padding: 0 11px;
}
.ux-status-filter:hover,
.ux-view-choice:hover {
border-color: var(--vd-line-strong);
color: var(--vd-ink);
}
.ux-status-filter.is-active,
.ux-view-choice.is-active {
background: var(--vd-brand-soft);
border-color: color-mix(in srgb, var(--vd-brand) 38%, var(--vd-line));
color: var(--vd-brand);
}
.ux-article-view-switch {
background: var(--vd-surface-muted);
border-radius: 999px;
justify-self: end;
padding: 3px;
}
.ux-view-choice {
background: transparent;
border-color: transparent;
min-width: 62px;
}
.ux-view-choice.is-active {
background: var(--vd-surface-raised);
border-color: var(--vd-line);
box-shadow: var(--vd-shadow-sm);
color: var(--vd-ink);
}
.ux-active-filters {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: -3px 0 13px;
min-height: 28px;
}
.ux-active-filters.is-empty {
display: none;
}
.ux-active-filters > strong {
color: var(--vd-ink-muted);
font-size: 9px;
margin-right: 2px;
text-transform: uppercase;
}
.ux-filter-chip {
align-items: center;
background: var(--vd-surface-muted);
border: 1px solid var(--vd-line);
border-radius: 999px;
color: var(--vd-ink-secondary);
display: inline-flex;
font-size: 9px;
gap: 5px;
min-height: 25px;
padding: 0 4px 0 9px;
}
.ux-filter-chip-clear {
align-items: center;
background: transparent;
border: 0;
border-radius: 50%;
color: var(--vd-ink-muted);
cursor: pointer;
display: inline-flex;
font-size: 15px;
height: 20px;
justify-content: center;
line-height: 1;
padding: 0;
width: 20px;
}
.ux-filter-chip-clear:hover {
background: var(--vd-surface-raised);
color: var(--vd-danger);
}
#history .history-batch-bar:not(.has-selection) .history-batch-actions,
#history .history-batch-bar:not(.has-selection) .history-clear-selection {
display: none;
}
#history .history-batch-bar:not(.has-selection) {
min-height: 43px;
}
#history .history-batch-bar.has-selection {
position: sticky;
top: calc(var(--vd-topbar-height) + 8px);
z-index: 25;
}
#history .history-batch-actions,
.ux-batch-primary,
.ux-batch-secondary {
align-items: center;
display: flex;
gap: 6px;
}
.ux-batch-more {
position: relative;
}
.ux-batch-more > summary {
align-items: center;
border: 1px solid var(--vd-line);
border-radius: 7px;
color: var(--vd-ink-secondary);
cursor: pointer;
display: inline-flex;
font-size: 10px;
font-weight: 650;
list-style: none;
min-height: 32px;
padding: 0 10px;
}
.ux-batch-more > summary::-webkit-details-marker {
display: none;
}
.ux-batch-more > summary::after {
content: '⌄';
margin-left: 7px;
}
.ux-batch-secondary {
background: var(--vd-surface-raised);
border: 1px solid var(--vd-line);
border-radius: var(--vd-radius-sm);
box-shadow: var(--vd-shadow-float);
flex-direction: column;
min-width: 190px;
padding: 7px;
position: absolute;
right: 0;
top: calc(100% + 7px);
z-index: 45;
}
.ux-batch-secondary .vd-button {
justify-content: flex-start;
width: 100%;
}
#history[data-article-view="cards"] .history-table-shell {
background: transparent;
border: 0;
overflow: visible;
}
#history[data-article-view="cards"] .history-batch-bar,
#history[data-article-view="cards"] .history-table-footer {
background: var(--vd-surface-raised);
border: 1px solid var(--vd-line);
border-radius: var(--vd-radius-md);
}
#history[data-article-view="cards"] .history-batch-bar {
margin-bottom: 12px;
}
#history[data-article-view="cards"] .history-table-footer {
margin-top: 12px;
}
#history[data-article-view="cards"] .history-table {
min-width: 0;
overflow: visible;
}
#history[data-article-view="cards"] .history-table-head {
display: none;
}
#history[data-article-view="cards"] .history-list {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fill, minmax(var(--ux-article-card-min), 1fr));
}
#history[data-article-view="cards"] .history-row {
align-content: start;
background: var(--vd-surface-raised);
border: 1px solid var(--vd-line);
border-radius: var(--vd-radius-md);
box-shadow: none;
display: grid;
gap: 0 12px;
grid-template-areas:
"select article menu"
". platform price"
". status published"
". location provider";
grid-template-columns: 22px minmax(0, 1fr) minmax(118px, .7fr);
min-height: 0;
padding: 14px;
transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}
#history[data-article-view="cards"] .history-row:hover {
background: var(--vd-surface-raised);
border-color: var(--vd-line-strong);
box-shadow: var(--vd-shadow-sm);
transform: translateY(-1px);
}
#history[data-article-view="cards"] .history-row.selected {
background: color-mix(in srgb, var(--vd-brand-soft) 42%, var(--vd-surface-raised));
border-color: color-mix(in srgb, var(--vd-brand) 45%, var(--vd-line));
box-shadow: 0 0 0 2px var(--vd-focus);
}
#history[data-article-view="cards"] .history-cell {
border-top: 1px solid var(--vd-line);
min-width: 0;
padding: 10px 0;
}
#history[data-article-view="cards"] .history-cell::before {
color: var(--vd-ink-muted);
content: attr(data-ux-label);
display: block;
font-size: 7.5px;
font-weight: 760;
letter-spacing: .06em;
margin-bottom: 4px;
text-transform: uppercase;
}
#history[data-article-view="cards"] .history-cell-select {
align-self: start;
border: 0;
grid-area: select;
padding: 3px 0 0;
}
#history[data-article-view="cards"] .history-cell-article {
align-items: center;
border: 0;
grid-area: article;
padding: 0 0 13px;
}
#history[data-article-view="cards"] .history-cell-article::before,
#history[data-article-view="cards"] .history-cell-menu::before {
display: none;
}
#history[data-article-view="cards"] .history-cell-platform { grid-area: platform; }
#history[data-article-view="cards"] .history-cell-provider { grid-area: provider; }
#history[data-article-view="cards"] .history-cell-price { grid-area: price; }
#history[data-article-view="cards"] .history-cell-status { grid-area: status; }
#history[data-article-view="cards"] .history-cell-location { grid-area: location; }
#history[data-article-view="cards"] .history-cell-published { grid-area: published; }
#history[data-article-view="cards"] .history-cell-menu {
align-self: start;
border: 0;
grid-area: menu;
justify-self: end;
padding: 0;
}
#history[data-article-view="cards"] .history-thumb {
border-radius: 9px;
height: 64px;
width: 72px;
}
#history[data-article-view="cards"] .history-article-title {
font-size: 12px;
white-space: normal;
}
#history[data-article-view="cards"] .history-article-sku {
margin-top: 6px;
}
#history[data-article-view="cards"] .history-cell-provider,
#history[data-article-view="cards"] .history-cell-location,
#history[data-article-view="cards"] .history-cell-platform,
#history[data-article-view="cards"] .history-cell-price,
#history[data-article-view="cards"] .history-cell-status,
#history[data-article-view="cards"] .history-cell-published {
align-items: flex-start;
display: block;
}
#history[data-article-view="cards"] .history-status-select {
max-width: 100%;
}
#history[data-article-view="cards"] .history-publish-badges {
flex-wrap: wrap;
}
#history[data-article-view="cards"] .history-row[data-publish-state="none"] .history-cell-published {
opacity: .72;
}
#history[data-article-view="list"] .history-table-head {
background: var(--vd-surface-raised);
position: sticky;
top: var(--vd-topbar-height);
z-index: 12;
}
#history[data-article-view="list"] .history-row:focus-visible,
#history[data-article-view="cards"] .history-row:focus-visible,
.ux-status-filter:focus-visible,
.ux-view-choice:focus-visible,
.ux-filter-chip-clear:focus-visible,
.ux-batch-more > summary:focus-visible {
outline: 3px solid var(--vd-focus);
outline-offset: 2px;
}
@media (max-width: 1180px) {
.ux-article-toolbar {
grid-template-columns: 1fr auto;
}
.ux-status-filters {
grid-column: 1 / -1;
grid-row: 2;
}
.ux-article-view-switch {
grid-column: 2;
grid-row: 1;
}
}
@media (max-width: 760px) {
.ux-article-toolbar {
align-items: stretch;
grid-template-columns: 1fr;
}
.ux-status-filters,
.ux-article-view-switch {
grid-column: auto;
grid-row: auto;
justify-self: stretch;
}
.ux-status-filters {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ux-status-filter,
.ux-view-choice {
width: 100%;
}
.ux-article-view-switch {
display: grid;
grid-template-columns: 1fr 1fr;
}
#history .history-batch-bar.has-selection {
align-items: stretch;
flex-direction: column;
position: static;
}
#history .history-batch-actions,
.ux-batch-primary {
align-items: stretch;
flex-direction: column;
width: 100%;
}
.ux-batch-primary .vd-button,
.ux-batch-more,
.ux-batch-more > summary {
justify-content: center;
width: 100%;
}
.ux-batch-secondary {
left: 0;
right: auto;
width: 100%;
}
#history[data-article-view="cards"] {
--ux-article-card-min: 280px;
}
}
@media (max-width: 520px) {
#history[data-article-view="cards"] .history-list {
grid-template-columns: minmax(0, 1fr);
}
#history[data-article-view="cards"] .history-row {
grid-template-areas:
"select article menu"
". platform price"
". status status"
". published published"
". location location"
". provider provider";
grid-template-columns: 20px minmax(0, 1fr) auto;
padding: 12px;
}
#history[data-article-view="cards"] .history-thumb {
height: 58px;
width: 64px;
}
}
@media (prefers-reduced-motion: reduce) {
#history[data-article-view="cards"] .history-row {
transition: none;
}
}
+317
View File
@@ -0,0 +1,317 @@
(() => {
'use strict';
const VIEW_STORAGE_KEY = 'vendoo_article_overview_view_v1';
const STATUS_FILTERS = [
{ value: '', label: 'Alle' },
{ value: 'active', label: 'Aktiv' },
{ value: 'reserved', label: 'Reserviert' },
{ value: 'sold', label: 'Verkauft' },
];
const CELL_LABELS = {
platform: 'Plattform',
provider: 'AI',
price: 'Preis',
status: 'Status',
location: 'Lagerort',
published: 'Veröffentlichung',
};
let historyObserver = null;
let metaObserver = null;
let refreshQueued = false;
function historySection() {
return document.getElementById('history');
}
function createButton(label, className, attributes = {}) {
const button = document.createElement('button');
button.type = 'button';
button.className = className;
button.textContent = label;
for (const [name, value] of Object.entries(attributes)) button.setAttribute(name, value);
return button;
}
function currentView() {
const stored = localStorage.getItem(VIEW_STORAGE_KEY);
return stored === 'list' || stored === 'cards' ? stored : 'cards';
}
function applyView(mode, { persist = true } = {}) {
const section = historySection();
if (!section) return;
const normalized = mode === 'list' ? 'list' : 'cards';
section.dataset.articleView = normalized;
section.querySelectorAll('[data-article-view-choice]').forEach(button => {
const active = button.dataset.articleViewChoice === normalized;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', String(active));
});
if (persist) localStorage.setItem(VIEW_STORAGE_KEY, normalized);
}
function setStatusFilter(value) {
const select = document.getElementById('filter-status');
if (!select) return;
select.value = value;
select.dispatchEvent(new Event('change', { bubbles: true }));
queueRefresh();
}
function createOverviewToolbar() {
const section = historySection();
const filterbar = section?.querySelector('.history-filterbar');
if (!section || !filterbar || section.querySelector('.ux-article-toolbar')) return;
const toolbar = document.createElement('section');
toolbar.className = 'ux-article-toolbar';
toolbar.setAttribute('aria-label', 'Artikelansicht und Schnellfilter');
const summary = document.createElement('div');
summary.className = 'ux-article-summary';
const summaryTitle = document.createElement('strong');
summaryTitle.textContent = 'Artikelübersicht';
const summaryCount = document.createElement('span');
summaryCount.id = 'ux-article-result-count';
summaryCount.textContent = 'Artikel werden geladen …';
summary.append(summaryTitle, summaryCount);
const filters = document.createElement('div');
filters.className = 'ux-status-filters';
filters.setAttribute('role', 'group');
filters.setAttribute('aria-label', 'Status schnell filtern');
for (const filter of STATUS_FILTERS) {
const button = createButton(filter.label, 'ux-status-filter', {
'data-article-status': filter.value,
'aria-pressed': 'false',
});
button.addEventListener('click', () => setStatusFilter(filter.value));
filters.append(button);
}
const viewSwitch = document.createElement('div');
viewSwitch.className = 'ux-article-view-switch';
viewSwitch.setAttribute('role', 'group');
viewSwitch.setAttribute('aria-label', 'Darstellung wählen');
const cards = createButton('Karten', 'ux-view-choice', {
'data-article-view-choice': 'cards',
'aria-pressed': 'false',
});
const list = createButton('Liste', 'ux-view-choice', {
'data-article-view-choice': 'list',
'aria-pressed': 'false',
});
cards.addEventListener('click', () => applyView('cards'));
list.addEventListener('click', () => applyView('list'));
viewSwitch.append(cards, list);
toolbar.append(summary, filters, viewSwitch);
const activeFilters = document.createElement('div');
activeFilters.className = 'ux-active-filters';
activeFilters.id = 'ux-active-filters';
activeFilters.setAttribute('aria-live', 'polite');
filterbar.before(toolbar);
filterbar.after(activeFilters);
}
function enhanceBatchActions() {
const actions = document.querySelector('#history .history-batch-actions');
if (!actions || actions.querySelector('.ux-batch-primary')) return;
const primary = document.createElement('div');
primary.className = 'ux-batch-primary';
const more = document.createElement('details');
more.className = 'ux-batch-more';
const summary = document.createElement('summary');
summary.textContent = 'Weitere Aktionen';
const secondary = document.createElement('div');
secondary.className = 'ux-batch-secondary';
more.append(summary, secondary);
const primaryIds = ['history-publish-selected', 'history-edit-selected', 'history-bulk-status'];
const secondaryIds = ['history-price-selected', 'history-duplicate-selected', 'history-print-labels', 'history-delete-selected'];
for (const id of primaryIds) {
const button = document.getElementById(id);
if (button) primary.append(button);
}
for (const id of secondaryIds) {
const button = document.getElementById(id);
if (button) secondary.append(button);
}
actions.append(primary, more);
}
function annotateRows() {
const container = document.getElementById('history-list');
if (!container) return;
container.querySelectorAll('.history-row').forEach(row => {
for (const [column, label] of Object.entries(CELL_LABELS)) {
row.querySelector(`[data-column="${column}"]`)?.setAttribute('data-ux-label', label);
}
const title = row.querySelector('.history-article-title')?.textContent?.trim() || 'Artikel';
row.tabIndex = 0;
row.setAttribute('aria-label', `${title} öffnen`);
row.dataset.publishState = row.querySelector('.history-publish-empty') ? 'none' : 'available';
if (row.dataset.uxKeyboardBound !== 'true') {
row.dataset.uxKeyboardBound = 'true';
row.addEventListener('keydown', event => {
if ((event.key === 'Enter' || event.key === ' ') && event.target === row) {
event.preventDefault();
row.querySelector('.history-article-title')?.click();
}
});
}
});
}
function syncStatusButtons() {
const value = document.getElementById('filter-status')?.value || '';
document.querySelectorAll('[data-article-status]').forEach(button => {
const active = button.dataset.articleStatus === value;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', String(active));
});
}
function selectedOptionText(select) {
if (!select || !select.value) return '';
return select.options[select.selectedIndex]?.textContent?.trim() || select.value;
}
function createFilterChip(label, onClear) {
const chip = document.createElement('span');
chip.className = 'ux-filter-chip';
const text = document.createElement('span');
text.textContent = label;
const clear = createButton('×', 'ux-filter-chip-clear', { 'aria-label': `${label} entfernen` });
clear.addEventListener('click', onClear);
chip.append(text, clear);
return chip;
}
function clearControl(control, value = '') {
if (!control) return;
control.value = value;
control.dispatchEvent(new Event(control.tagName === 'INPUT' ? 'input' : 'change', { bubbles: true }));
queueRefresh();
}
function updateActiveFilters() {
const container = document.getElementById('ux-active-filters');
if (!container) return;
container.replaceChildren();
const query = document.getElementById('search-input');
const platform = document.getElementById('filter-platform');
const status = document.getElementById('filter-status');
const period = document.getElementById('history-period');
const dateFrom = document.getElementById('filter-date-from');
const dateTo = document.getElementById('filter-date-to');
const chips = [];
if (query?.value.trim()) chips.push(createFilterChip(`Suche: ${query.value.trim()}`, () => clearControl(query)));
if (platform?.value) chips.push(createFilterChip(`Plattform: ${selectedOptionText(platform)}`, () => clearControl(platform)));
if (status?.value) chips.push(createFilterChip(`Status: ${selectedOptionText(status)}`, () => clearControl(status)));
if (period?.value && period.value !== '90' && period.value !== 'custom') {
chips.push(createFilterChip(`Zeitraum: ${selectedOptionText(period)}`, () => clearControl(period, '90')));
}
if (dateFrom?.value) chips.push(createFilterChip(`Von: ${dateFrom.value}`, () => clearControl(dateFrom)));
if (dateTo?.value) chips.push(createFilterChip(`Bis: ${dateTo.value}`, () => clearControl(dateTo)));
if (!chips.length) {
container.classList.add('is-empty');
return;
}
container.classList.remove('is-empty');
const label = document.createElement('strong');
label.textContent = 'Aktive Filter';
container.append(label, ...chips);
}
function updateResultCount() {
const target = document.getElementById('ux-article-result-count');
if (!target) return;
const info = document.getElementById('history-total-info')?.textContent?.trim() || '';
const match = info.match(/von\s+(\d+)\s+Eintr/i);
const visible = document.querySelectorAll('#history-list .history-row').length;
if (match) target.textContent = `${match[1]} Artikel gefunden`;
else target.textContent = visible === 1 ? '1 Artikel gefunden' : `${visible} Artikel gefunden`;
}
function syncBatchState() {
const section = historySection();
const countText = document.getElementById('history-selected-count')?.textContent || '0';
const count = Number.parseInt(countText, 10) || 0;
section?.toggleAttribute('data-has-selection', count > 0);
const more = section?.querySelector('.ux-batch-more');
if (!count && more) more.open = false;
}
function refreshPresentation() {
refreshQueued = false;
annotateRows();
syncStatusButtons();
updateActiveFilters();
updateResultCount();
syncBatchState();
}
function queueRefresh() {
if (refreshQueued) return;
refreshQueued = true;
window.requestAnimationFrame(refreshPresentation);
}
function bindFilterState() {
const ids = ['search-input', 'filter-platform', 'filter-status', 'history-period', 'filter-date-from', 'filter-date-to'];
for (const id of ids) {
const control = document.getElementById(id);
if (!control || control.dataset.uxArticleBound === 'true') continue;
control.dataset.uxArticleBound = 'true';
control.addEventListener('input', queueRefresh);
control.addEventListener('change', queueRefresh);
}
document.getElementById('history-reset-filters')?.addEventListener('click', () => window.setTimeout(queueRefresh, 0));
const search = document.getElementById('search-input');
if (search) search.placeholder = 'Titel, SKU, Marke oder Beschreibung durchsuchen';
}
function observeArticleOverview() {
const list = document.getElementById('history-list');
const shell = document.querySelector('#history .history-table-shell');
if (list) {
historyObserver = new MutationObserver(queueRefresh);
historyObserver.observe(list, { childList: true, subtree: true });
}
if (shell) {
metaObserver = new MutationObserver(queueRefresh);
metaObserver.observe(shell, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['class'] });
}
window.addEventListener('beforeunload', () => {
historyObserver?.disconnect();
metaObserver?.disconnect();
}, { once: true });
}
function initializeArticleOverview() {
const section = historySection();
if (!section || section.classList.contains('ux-article-overview-ready')) return;
createOverviewToolbar();
enhanceBatchActions();
bindFilterState();
applyView(currentView(), { persist: false });
observeArticleOverview();
refreshPresentation();
section.classList.add('ux-article-overview-ready');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeArticleOverview, { once: true });
} else {
initializeArticleOverview();
}
})();
+1
View File
@@ -0,0 +1 @@
.ux-command-platform-chrome{display:flex;align-items:center;gap:10px;min-height:42px;padding:7px 18px;border-bottom:1px solid var(--vd-color-border,rgba(127,127,127,.2));background:var(--vd-color-surface,#fff);position:relative;z-index:20}.ux-command-breadcrumb{display:flex;align-items:center;gap:8px;min-width:0;flex:1}.ux-command-crumb,.ux-command-favorite-toggle,.ux-command-drawer-toggle,.ux-command-drawer-item{font:inherit;color:inherit;border:1px solid var(--vd-color-border,rgba(127,127,127,.25));background:var(--vd-color-surface-raised,var(--vd-color-surface,#fff));border-radius:var(--vd-radius-sm,8px)}.ux-command-crumb{border:0;background:transparent;padding:4px 0;font-weight:650}.ux-command-crumb-separator{opacity:.5}.ux-command-crumb-current{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--vd-color-text-muted,#666);font-size:.9rem}.ux-command-favorite-toggle{width:34px;height:30px;padding:0;font-size:1rem}.ux-command-drawer-toggle{padding:6px 10px;font-size:.84rem}.ux-command-drawer{position:absolute;right:18px;top:48px;width:min(360px,calc(100vw - 28px));max-height:min(520px,70vh);overflow:auto;padding:12px;border:1px solid var(--vd-color-border,rgba(127,127,127,.25));border-radius:var(--vd-radius-lg,14px);background:var(--vd-color-surface-raised,var(--vd-color-surface,#fff));box-shadow:var(--vd-shadow-lg,0 18px 48px rgba(0,0,0,.18));display:grid;gap:14px}.ux-command-drawer.hidden{display:none}.ux-command-drawer-section{display:grid;gap:7px}.ux-command-drawer-section>strong{font-size:.75rem;letter-spacing:.05em;text-transform:uppercase;color:var(--vd-color-text-muted,#666)}.ux-command-drawer-item{text-align:left;padding:9px 10px;cursor:pointer}.ux-command-drawer-empty{padding:8px 2px;color:var(--vd-color-text-muted,#777);font-size:.85rem}.ux-command-crumb:hover,.ux-command-favorite-toggle:hover,.ux-command-drawer-toggle:hover,.ux-command-drawer-item:hover{background:var(--vd-color-surface-hover,rgba(127,127,127,.09))}.ux-command-crumb:focus-visible,.ux-command-favorite-toggle:focus-visible,.ux-command-drawer-toggle:focus-visible,.ux-command-drawer-item:focus-visible{outline:2px solid var(--vd-color-focus,#4f7cff);outline-offset:2px}@media(max-width:780px){.ux-command-platform-chrome{padding-inline:12px}.ux-command-drawer-toggle{font-size:0;width:34px;height:30px;padding:0}.ux-command-drawer-toggle::before{content:'★';font-size:1rem}.ux-command-crumb-current{font-size:.82rem}}@media(max-width:520px){.ux-command-platform-chrome{gap:6px}.ux-command-crumb-separator{display:none}.ux-command-crumb-current{max-width:48vw}.ux-command-drawer{right:8px;top:46px;width:calc(100vw - 16px)}}@media(prefers-reduced-motion:reduce){.ux-command-platform-chrome *{scroll-behavior:auto!important;transition:none!important;animation:none!important}}
+453
View File
@@ -0,0 +1,453 @@
(() => {
'use strict';
if (document.documentElement.dataset.uxCommandPlatformReady === '1') return;
document.documentElement.dataset.uxCommandPlatformReady = '1';
const FAVORITES_KEY = 'vendoo_command_favorites_v1';
const RECENTS_KEY = 'vendoo_navigation_recents_v1';
const MAX_RECENTS = 12;
const registry = new Map();
const listeners = new Set();
const VIEW_META = {
dashboard: { title: 'Übersicht', group: 'Start', icon: '⌂', keywords: ['dashboard', 'start'], aliases: ['home'] },
today: { title: 'Aufgaben', group: 'Start', icon: '✓', keywords: ['heute', 'tagesarbeitsplatz'], aliases: ['today'] },
generator: { title: 'Neuer Artikel', group: 'Erstellen', icon: '+', keywords: ['generator', 'listing erstellen'], aliases: ['artikel erstellen'] },
history: { title: 'Artikel', group: 'Verkaufen', icon: '□', keywords: ['listings', 'artikelübersicht'], aliases: ['historie'] },
'quality-center': { title: 'Qualität', group: 'Optimieren', icon: '◇', keywords: ['qualitätscenter', 'prüfen'], aliases: ['quality'] },
publish: { title: 'Veröffentlichen', group: 'Verkaufen', icon: '↗', keywords: ['publish', 'marktplätze'], aliases: ['publishing'] },
inventory: { title: 'Lager', group: 'Bestand', icon: '▣', keywords: ['bestand', 'lagerorte'], aliases: ['inventory'] },
templates: { title: 'Vorlagen', group: 'Inhalte', icon: '▤', keywords: ['templates'], aliases: ['template'] },
'media-library': { title: 'Medien', group: 'Inhalte', icon: '▧', keywords: ['bilder', 'galerie'], aliases: ['medienbibliothek'] },
'image-factory': { title: 'Bildwerkstatt', group: 'AI & Bilder', icon: '✦', keywords: ['bildproduktion'], aliases: ['image factory'] },
'flux-studio': { title: 'FLUX Studio', group: 'AI & Bilder', icon: '✧', keywords: ['comfyui', 'ai bilder'], aliases: ['flux'] },
extensions: { title: 'Erweiterungen', group: 'System', icon: '⌘', keywords: ['extensions', 'browser'], aliases: ['browser erweiterung'] },
settings: { title: 'Einstellungen', group: 'System', icon: '⚙', keywords: ['verbindungen', 'apis'], aliases: ['settings'] },
admin: { title: 'Verwaltung', group: 'System', icon: '◈', keywords: ['benutzer', 'admin'], aliases: ['administration'] },
fees: { title: 'Gebühren', group: 'Verkaufen', icon: '€', keywords: ['kosten'], aliases: ['fees'] },
trash: { title: 'Papierkorb', group: 'System', icon: '⌫', keywords: ['gelöscht'], aliases: ['trash'] },
};
const readList = key => {
try {
const value = JSON.parse(localStorage.getItem(key) || '[]');
return Array.isArray(value) ? value.filter(item => typeof item === 'string') : [];
} catch {
return [];
}
};
const writeList = (key, value) => {
try { localStorage.setItem(key, JSON.stringify(value)); } catch {}
};
let favorites = readList(FAVORITES_KEY);
let recents = readList(RECENTS_KEY);
const emit = (type, detail = {}) => {
window.dispatchEvent(new CustomEvent(`vendoo:${type}`, { detail }));
listeners.forEach(listener => {
try { listener({ type, detail }); } catch {}
});
};
const normalizeSearchText = value => String(value || '')
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.toLocaleLowerCase('de-DE')
.trim();
const normalizeStringList = value => Array.isArray(value)
? [...new Set(value.map(item => String(item || '').trim()).filter(Boolean))]
: [];
function normalizeCommand(input) {
if (!input || typeof input !== 'object') throw new TypeError('Command muss ein Objekt sein.');
const id = String(input.id || '').trim();
const title = String(input.title || '').trim();
if (!id || !title) throw new TypeError('Command benötigt id und title.');
return Object.freeze({
id,
title,
description: String(input.description || ''),
group: String(input.group || 'Allgemein'),
icon: String(input.icon || '•'),
keywords: normalizeStringList(input.keywords),
aliases: normalizeStringList(input.aliases),
shortcut: String(input.shortcut || ''),
permission: input.permission ?? null,
visible: input.visible ?? null,
enabled: input.enabled ?? null,
disabledReason: String(input.disabledReason || ''),
kind: String(input.kind || 'navigation'),
view: input.view ? String(input.view) : null,
execute: typeof input.execute === 'function' ? input.execute : null,
});
}
function register(command) {
const normalized = normalizeCommand(command);
registry.set(normalized.id, normalized);
emit('commands-changed', { commandId: normalized.id });
return () => unregister(normalized.id);
}
function unregister(id) {
const commandId = String(id);
const removed = registry.delete(commandId);
if (removed) {
favorites = favorites.filter(item => item !== commandId);
writeList(FAVORITES_KEY, favorites);
emit('commands-changed', { commandId });
}
return removed;
}
function getActiveView() {
return document.querySelector('.sidebar-nav .nav-item.active[data-tab]')?.dataset.tab
|| document.querySelector('.tab-content.active')?.id
|| 'dashboard';
}
function getNavItem(view) {
return document.querySelector(`.sidebar-nav .nav-item[data-tab="${CSS.escape(view)}"]`);
}
function canOpen(view) {
const item = getNavItem(view);
return Boolean(item && !item.classList.contains('hidden') && item.getAttribute('aria-hidden') !== 'true');
}
function openView(view) {
const item = getNavItem(view);
if (!item || !canOpen(view)) return false;
item.closest('details.ux-nav-section')?.setAttribute('open', '');
item.click();
return true;
}
function evaluateRule(rule, context, fallback) {
if (typeof rule === 'function') {
try { return Boolean(rule(context)); } catch { return false; }
}
if (typeof rule === 'boolean') return rule;
return fallback;
}
function evaluatePermission(permission, context) {
if (permission === null || permission === undefined || permission === '') return true;
if (typeof permission === 'function') {
try { return Boolean(permission(context)); } catch { return false; }
}
if (typeof permission === 'boolean') return permission;
const resolver = context?.canPermission || window.VendooPermissions?.can;
if (typeof resolver !== 'function') return false;
try {
if (Array.isArray(permission)) return permission.every(value => resolver.call(window.VendooPermissions, value, context));
return Boolean(resolver.call(window.VendooPermissions, permission, context));
} catch {
return false;
}
}
function resolveCommand(commandOrId, context = {}) {
const command = typeof commandOrId === 'string' ? registry.get(commandOrId) : commandOrId;
if (!command) return null;
const viewVisible = !command.view || canOpen(command.view);
const visible = viewVisible && evaluateRule(command.visible, context, true) && evaluatePermission(command.permission, context);
const enabled = visible && evaluateRule(command.enabled, context, true);
const disabledReason = enabled
? ''
: (command.disabledReason || (!viewVisible ? 'Dieser Bereich ist derzeit nicht verfügbar.' : 'Dieser Befehl ist derzeit nicht verfügbar.'));
return Object.freeze({ visible, enabled, disabledReason });
}
function subsequenceScore(value, needle) {
if (!value || !needle) return 0;
let cursor = 0;
let gaps = 0;
for (const character of needle) {
const next = value.indexOf(character, cursor);
if (next < 0) return 0;
gaps += next - cursor;
cursor = next + 1;
}
return Math.max(1, 90 - gaps);
}
function scoreField(value, needle, weights) {
const normalized = normalizeSearchText(value);
if (!normalized || !needle) return 0;
if (normalized === needle) return weights.exact;
if (normalized.startsWith(needle)) return weights.prefix;
if (normalized.split(/\s+/).some(token => token.startsWith(needle))) return weights.token;
if (normalized.includes(needle)) return weights.includes;
return Math.round(subsequenceScore(normalized, needle) * weights.fuzzy);
}
function scoreCommand(command, query) {
const needle = normalizeSearchText(query);
if (!needle) return 0;
const scores = [
scoreField(command.title, needle, { exact: 1200, prefix: 1000, token: 850, includes: 720, fuzzy: 1.8 }),
...command.aliases.map(value => scoreField(value, needle, { exact: 1100, prefix: 920, token: 800, includes: 680, fuzzy: 1.6 })),
...command.keywords.map(value => scoreField(value, needle, { exact: 820, prefix: 700, token: 620, includes: 520, fuzzy: 1.25 })),
scoreField(command.description, needle, { exact: 500, prefix: 430, token: 360, includes: 300, fuzzy: .8 }),
scoreField(command.group, needle, { exact: 360, prefix: 320, token: 280, includes: 230, fuzzy: .65 }),
];
return Math.max(0, ...scores);
}
function list({ query = '', group = null, context = {}, includeDisabled = true, includeHidden = false } = {}) {
const needle = normalizeSearchText(query);
return [...registry.values()]
.map(command => {
const status = resolveCommand(command, context);
const favoriteIndex = favorites.indexOf(command.id);
const recentView = command.view ? recents.indexOf(command.view) : -1;
const score = scoreCommand(command, needle)
+ (favoriteIndex >= 0 ? Math.max(40, 120 - favoriteIndex * 8) : 0)
+ (recentView >= 0 ? Math.max(20, 80 - recentView * 5) : 0);
return Object.freeze({
...command,
...status,
favorite: favoriteIndex >= 0,
favoriteIndex,
recent: recentView >= 0,
recentIndex: recentView,
score,
});
})
.filter(command => (!group || command.group === group)
&& (includeHidden || command.visible)
&& (includeDisabled || command.enabled)
&& (!needle || command.score > 0))
.sort((left, right) => {
if (needle && right.score !== left.score) return right.score - left.score;
if (!needle && left.favorite !== right.favorite) return left.favorite ? -1 : 1;
if (!needle && left.favorite && right.favorite && left.favoriteIndex !== right.favoriteIndex) return left.favoriteIndex - right.favoriteIndex;
if (!needle && left.recent !== right.recent) return left.recent ? -1 : 1;
if (!needle && left.recent && right.recent && left.recentIndex !== right.recentIndex) return left.recentIndex - right.recentIndex;
return left.title.localeCompare(right.title, 'de-DE', { sensitivity: 'base' });
});
}
function recordRecent(view) {
if (!view || !VIEW_META[view]) return;
recents = [view, ...recents.filter(item => item !== view)].slice(0, MAX_RECENTS);
writeList(RECENTS_KEY, recents);
renderNavigationChrome();
emit('navigation-changed', { view, recents: [...recents] });
}
function toggleFavorite(commandId) {
const id = String(commandId);
if (!registry.has(id)) return false;
favorites = favorites.includes(id) ? favorites.filter(item => item !== id) : [id, ...favorites];
writeList(FAVORITES_KEY, favorites);
renderNavigationChrome();
emit('favorites-changed', { commandId: id, favorites: [...favorites] });
return favorites.includes(id);
}
function execute(id, context = {}) {
const command = registry.get(String(id));
if (!command) return false;
const status = resolveCommand(command, context);
if (!status?.visible || !status.enabled) {
emit('command-blocked', { commandId: command.id, reason: status?.disabledReason || 'Befehl nicht verfügbar.' });
return false;
}
let result;
try {
result = command.execute ? command.execute(context) : (command.view ? openView(command.view) : false);
} catch (error) {
emit('command-failed', { commandId: command.id, message: error?.message || String(error) });
return false;
}
if (result !== false && command.view) recordRecent(command.view);
emit('command-executed', { commandId: command.id, view: command.view });
return result !== false;
}
function createButton(className, label, title) {
const button = document.createElement('button');
button.type = 'button';
button.className = className;
button.textContent = label;
button.title = title;
return button;
}
function ensureChrome() {
const topbar = document.getElementById('topbar');
if (!topbar || document.getElementById('ux-command-platform-chrome')) return;
const chrome = document.createElement('div');
chrome.id = 'ux-command-platform-chrome';
chrome.className = 'ux-command-platform-chrome';
const breadcrumb = document.createElement('nav');
breadcrumb.id = 'ux-command-breadcrumb';
breadcrumb.className = 'ux-command-breadcrumb';
breadcrumb.setAttribute('aria-label', 'Brotkrümelnavigation');
const favoriteButton = createButton('ux-command-favorite-toggle', '☆', 'Aktuellen Bereich zu Favoriten hinzufügen');
favoriteButton.id = 'ux-command-favorite-toggle';
favoriteButton.setAttribute('aria-pressed', 'false');
const drawerButton = createButton('ux-command-drawer-toggle', 'Favoriten', 'Favoriten und zuletzt geöffnete Bereiche anzeigen');
drawerButton.id = 'ux-command-drawer-toggle';
drawerButton.setAttribute('aria-expanded', 'false');
const drawer = document.createElement('section');
drawer.id = 'ux-command-drawer';
drawer.className = 'ux-command-drawer hidden';
drawer.setAttribute('aria-label', 'Favoriten und Verlauf');
favoriteButton.addEventListener('click', () => toggleFavorite(`view:${getActiveView()}`));
drawerButton.addEventListener('click', () => {
const open = drawer.classList.toggle('hidden') === false;
drawerButton.setAttribute('aria-expanded', open ? 'true' : 'false');
if (open) drawer.querySelector('button')?.focus({ preventScroll: true });
});
document.addEventListener('keydown', event => {
if (event.key === 'Escape' && !drawer.classList.contains('hidden')) {
drawer.classList.add('hidden');
drawerButton.setAttribute('aria-expanded', 'false');
drawerButton.focus({ preventScroll: true });
}
});
chrome.append(breadcrumb, favoriteButton, drawerButton, drawer);
topbar.insertAdjacentElement('afterend', chrome);
}
function renderNavigationChrome() {
ensureChrome();
const view = getActiveView();
const meta = VIEW_META[view] || { title: view, group: 'Vendoo' };
const breadcrumb = document.getElementById('ux-command-breadcrumb');
const favoriteButton = document.getElementById('ux-command-favorite-toggle');
const drawer = document.getElementById('ux-command-drawer');
if (!breadcrumb || !favoriteButton || !drawer) return;
breadcrumb.replaceChildren();
const home = createButton('ux-command-crumb', 'Vendoo', 'Zur Übersicht');
home.addEventListener('click', () => execute('view:dashboard'));
const separator = document.createElement('span');
separator.className = 'ux-command-crumb-separator';
separator.textContent = '';
separator.setAttribute('aria-hidden', 'true');
const current = document.createElement('span');
current.className = 'ux-command-crumb-current';
current.textContent = `${meta.group} · ${meta.title}`;
breadcrumb.append(home, separator, current);
const commandId = `view:${view}`;
const favorite = favorites.includes(commandId);
favoriteButton.textContent = favorite ? '★' : '☆';
favoriteButton.setAttribute('aria-pressed', favorite ? 'true' : 'false');
favoriteButton.title = favorite ? 'Aus Favoriten entfernen' : 'Aktuellen Bereich zu Favoriten hinzufügen';
drawer.replaceChildren();
const appendSection = (title, ids) => {
const section = document.createElement('div');
section.className = 'ux-command-drawer-section';
const heading = document.createElement('strong');
heading.textContent = title;
section.append(heading);
const valid = ids
.map(id => id.startsWith('view:') ? id : `view:${id}`)
.map(id => list().find(command => command.id === id))
.filter(Boolean);
if (!valid.length) {
const empty = document.createElement('span');
empty.className = 'ux-command-drawer-empty';
empty.textContent = title === 'Favoriten' ? 'Noch keine Favoriten angeheftet.' : 'Noch keine Bereiche geöffnet.';
section.append(empty);
} else {
valid.forEach(command => {
const button = createButton('ux-command-drawer-item', `${command.icon} ${command.title}`, command.description || command.title);
button.addEventListener('click', () => {
execute(command.id, { source: 'command-drawer' });
drawer.classList.add('hidden');
document.getElementById('ux-command-drawer-toggle')?.setAttribute('aria-expanded', 'false');
});
section.append(button);
});
}
drawer.append(section);
};
appendSection('Favoriten', favorites);
appendSection('Zuletzt geöffnet', recents);
}
function registerCoreNavigation() {
Object.entries(VIEW_META).forEach(([view, meta]) => {
register({
id: `view:${view}`,
title: meta.title,
description: `${meta.group} öffnen`,
group: meta.group,
icon: meta.icon,
keywords: meta.keywords,
aliases: meta.aliases,
kind: 'navigation',
view,
execute: () => openView(view),
});
});
}
function observeNavigation() {
const nav = document.querySelector('.sidebar-nav');
if (!nav) return;
let current = getActiveView();
recordRecent(current);
const observer = new MutationObserver(() => {
const next = getActiveView();
if (!next || next === current) return;
current = next;
recordRecent(next);
emit('commands-changed', { reason: 'navigation-visibility' });
});
nav.querySelectorAll('.nav-item[data-tab]').forEach(item => observer.observe(item, {
attributes: true,
attributeFilter: ['class', 'aria-hidden', 'disabled'],
}));
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
window.VendooCommands = Object.freeze({
register,
unregister,
list,
get: id => registry.get(String(id)) || null,
resolve: (id, context = {}) => resolveCommand(String(id), context),
canExecute: (id, context = {}) => Boolean(resolveCommand(String(id), context)?.enabled),
execute,
toggleFavorite,
isFavorite: id => favorites.includes(String(id)),
get favorites() { return [...favorites]; },
subscribe(listener) {
if (typeof listener !== 'function') throw new TypeError('Listener muss eine Funktion sein.');
listeners.add(listener);
return () => listeners.delete(listener);
},
});
window.VendooNavigation = Object.freeze({
get currentView() { return getActiveView(); },
get recents() { return [...recents]; },
open: openView,
back: () => history.back(),
forward: () => history.forward(),
home: () => execute('view:dashboard'),
});
registerCoreNavigation();
ensureChrome();
renderNavigationChrome();
observeNavigation();
})();
+168
View File
@@ -70,7 +70,175 @@ function restoreStored() {
return applyState(readStoredState(), { persist: false });
}
function loadUxFoundation() {
if (!document.querySelector('link[data-vendoo-ux-foundation]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/ux-foundation.css?v=1.44.0';
stylesheet.dataset.vendooUxFoundation = '1.44.0';
document.head.append(stylesheet);
}
const loadBehavior = () => {
if (document.querySelector('script[data-vendoo-ux-foundation]')) return;
const script = document.createElement('script');
script.src = '/ux-foundation.js?v=1.44.0';
script.dataset.vendooUxFoundation = '1.44.0';
script.async = false;
document.body.append(script);
};
if (document.readyState === 'complete') loadBehavior();
else window.addEventListener('load', loadBehavior, { once: true });
}
function loadFluxStudioAssets() {
if (!document.querySelector('link[data-ux-flux-studio]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/flux-studio-ux.css?v=1.44.0';
stylesheet.setAttribute('data-ux-flux-studio', 'style');
document.head.append(stylesheet);
}
const loadBehavior = () => {
if (document.querySelector('script[data-ux-flux-studio]')) return;
const script = document.createElement('script');
script.src = '/flux-studio-ux.js?v=1.44.0';
script.setAttribute('data-ux-flux-studio', 'script');
script.async = false;
document.body.append(script);
};
if (document.readyState === 'complete') loadBehavior();
else window.addEventListener('load', loadBehavior, { once: true });
}
function loadQualityCenterAssets() {
if (!document.querySelector('link[data-ux-quality-center]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/quality-center-ux.css?v=1.44.0';
stylesheet.setAttribute('data-ux-quality-center', 'style');
document.head.append(stylesheet);
}
const loadBehavior = () => {
if (document.querySelector('script[data-ux-quality-center]')) return;
const script = document.createElement('script');
script.src = '/quality-center-ux.js?v=1.44.0';
script.setAttribute('data-ux-quality-center', 'script');
script.async = false;
document.body.append(script);
};
if (document.readyState === 'complete') loadBehavior();
else window.addEventListener('load', loadBehavior, { once: true });
}
function loadInventoryWorkspaceAssets() {
if (!document.querySelector('link[data-ux-inventory-workspace]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/inventory-workspace.css?v=1.44.0';
stylesheet.setAttribute('data-ux-inventory-workspace', 'style');
document.head.append(stylesheet);
}
const loadBehavior = () => {
if (document.querySelector('script[data-ux-inventory-workspace]')) return;
const script = document.createElement('script');
script.src = '/inventory-workspace.js?v=1.44.0';
script.setAttribute('data-ux-inventory-workspace', 'script');
script.async = false;
document.body.append(script);
};
if (document.readyState === 'complete') loadBehavior();
else window.addEventListener('load', loadBehavior, { once: true });
}
function loadTemplateWorkspaceAssets() {
if (!document.querySelector('link[data-ux-template-workspace]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/template-workspace.css?v=1.44.0';
stylesheet.setAttribute('data-ux-template-workspace', 'style');
document.head.append(stylesheet);
}
const loadBehavior = () => {
if (document.querySelector('script[data-ux-template-workspace]')) return;
const script = document.createElement('script');
script.src = '/template-workspace.js?v=1.44.0';
script.setAttribute('data-ux-template-workspace', 'script');
script.async = false;
document.body.append(script);
};
if (document.readyState === 'complete') loadBehavior();
else window.addEventListener('load', loadBehavior, { once: true });
}
function loadTodayWorkspaceAssets() {
if (!document.querySelector('link[data-ux-today-workspace]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/today-workspace.css?v=1.44.0';
stylesheet.setAttribute('data-ux-today-workspace', 'style');
document.head.append(stylesheet);
}
const loadBehavior = () => {
if (document.querySelector('script[data-ux-today-workspace]')) return;
const script = document.createElement('script');
script.src = '/today-workspace.js?v=1.44.0';
script.setAttribute('data-ux-today-workspace', 'script');
script.async = false;
document.body.append(script);
};
if (document.readyState === 'complete') loadBehavior();
else window.addEventListener('load', loadBehavior, { once: true });
}
function loadSettingsControlCenterAssets() {
if (!document.querySelector('link[data-ux-settings-control-center]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/settings-control-center.css?v=1.44.0';
stylesheet.setAttribute('data-ux-settings-control-center', 'style');
document.head.append(stylesheet);
}
const loadBehavior = () => {
if (document.querySelector('script[data-ux-settings-control-center]')) return;
const script = document.createElement('script');
script.src = '/settings-control-center.js?v=1.44.0';
script.setAttribute('data-ux-settings-control-center', 'script');
script.async = false;
document.body.append(script);
};
if (document.readyState === 'complete') loadBehavior();
else window.addEventListener('load', loadBehavior, { once: true });
}
function loadModuleExtensionWorkspaceAssets() {
if (!document.querySelector('link[data-ux-module-extension-workspace]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/module-extension-workspace.css?v=1.44.0';
stylesheet.setAttribute('data-ux-module-extension-workspace', 'style');
document.head.append(stylesheet);
}
const loadBehavior = () => {
if (document.querySelector('script[data-ux-module-extension-workspace]')) return;
const script = document.createElement('script');
script.src = '/module-extension-workspace.js?v=1.44.0';
script.setAttribute('data-ux-module-extension-workspace', 'script');
script.async = false;
document.body.append(script);
};
if (document.readyState === 'complete') loadBehavior();
else window.addEventListener('load', loadBehavior, { once: true });
}
applyState(readStoredState(), { persist: false, dispatch: false });
loadUxFoundation();
loadFluxStudioAssets();
loadQualityCenterAssets();
loadInventoryWorkspaceAssets();
loadTemplateWorkspaceAssets();
loadTodayWorkspaceAssets();
loadSettingsControlCenterAssets();
loadModuleExtensionWorkspaceAssets();
window.matchMedia?.('(prefers-color-scheme: dark)').addEventListener?.('change', () => {
if (document.documentElement.dataset.themePreference === 'system') restoreStored();
});
+537
View File
@@ -0,0 +1,537 @@
#flux-studio.ux-flux-studio-ready {
--ux-flux-gap: clamp(14px, 1.6vw, 22px);
}
#flux-studio.ux-flux-studio-ready .flux-studio-heading {
align-items: flex-start;
gap: var(--ux-flux-gap);
}
#flux-studio.ux-flux-studio-ready .flux-studio-engine-actions {
flex: 0 0 auto;
}
.ux-flux-view-navigation {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin: 0 0 var(--ux-flux-gap);
padding: 6px;
border: 1px solid var(--vd-border, rgba(28, 25, 23, .12));
border-radius: var(--vd-radius-lg, 16px);
background: var(--vd-surface, #fff);
box-shadow: var(--vd-shadow-sm, 0 8px 24px rgba(28, 25, 23, .06));
}
.ux-flux-view-tab {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-width: 0;
padding: 13px 15px;
border: 1px solid transparent;
border-radius: calc(var(--vd-radius-lg, 16px) - 5px);
background: transparent;
color: var(--vd-text, #292524);
text-align: left;
cursor: pointer;
transition: background-color .16s ease, border-color .16s ease, transform .16s ease;
}
.ux-flux-view-tab:hover {
background: var(--vd-surface-muted, rgba(120, 113, 108, .08));
}
.ux-flux-view-tab.is-active {
border-color: color-mix(in srgb, var(--vd-accent, #d85f35) 32%, transparent);
background: color-mix(in srgb, var(--vd-accent, #d85f35) 10%, var(--vd-surface, #fff));
}
.ux-flux-view-copy {
display: grid;
gap: 2px;
min-width: 0;
}
.ux-flux-view-copy strong {
font-size: .93rem;
}
.ux-flux-view-copy small {
overflow: hidden;
color: var(--vd-text-muted, #78716c);
font-size: .76rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.ux-flux-view-badge:empty {
display: none;
}
.ux-flux-view-badge {
flex: 0 0 auto;
min-width: 26px;
padding: 4px 8px;
border-radius: 999px;
background: var(--vd-surface-strong, #f1ede9);
color: var(--vd-text-muted, #57534e);
font-size: .7rem;
font-weight: 750;
text-align: center;
}
.ux-flux-view-badge[data-tone="working"] {
background: color-mix(in srgb, var(--vd-warning, #c9801c) 16%, transparent);
color: var(--vd-warning, #9a5b0c);
}
.ux-flux-view-badge[data-tone="danger"] {
background: color-mix(in srgb, var(--vd-danger, #c44235) 14%, transparent);
color: var(--vd-danger, #b1362c);
}
.ux-flux-workflow {
display: grid;
grid-template-columns: minmax(240px, .85fr) minmax(480px, 1.8fr) auto;
align-items: center;
gap: var(--ux-flux-gap);
margin-bottom: var(--ux-flux-gap);
padding: clamp(16px, 2vw, 24px);
border: 1px solid var(--vd-border, rgba(28, 25, 23, .12));
border-radius: var(--vd-radius-xl, 20px);
background: linear-gradient(135deg, color-mix(in srgb, var(--vd-accent, #d85f35) 8%, var(--vd-surface, #fff)), var(--vd-surface, #fff));
box-shadow: var(--vd-shadow-sm, 0 8px 24px rgba(28, 25, 23, .06));
}
.ux-flux-workflow-copy {
display: grid;
gap: 5px;
}
.ux-flux-kicker {
color: var(--vd-accent, #d85f35);
font-size: .7rem;
font-weight: 800;
letter-spacing: .1em;
text-transform: uppercase;
}
.ux-flux-workflow-copy h3,
.ux-flux-workflow-copy p {
margin: 0;
}
.ux-flux-workflow-copy h3 {
font-size: clamp(1rem, 1.4vw, 1.22rem);
}
.ux-flux-workflow-copy p {
color: var(--vd-text-muted, #78716c);
font-size: .82rem;
line-height: 1.45;
}
.ux-flux-steps {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.ux-flux-step {
position: relative;
display: grid;
grid-template-columns: auto 1fr;
gap: 2px 8px;
min-width: 0;
padding: 10px;
border: 1px solid var(--vd-border, rgba(28, 25, 23, .1));
border-radius: var(--vd-radius-md, 12px);
background: color-mix(in srgb, var(--vd-surface, #fff) 78%, transparent);
color: var(--vd-text, #292524);
text-align: left;
cursor: pointer;
}
.ux-flux-step-index {
grid-row: 1 / span 2;
display: grid;
place-items: center;
width: 25px;
height: 25px;
border-radius: 50%;
background: var(--vd-surface-strong, #eee9e4);
color: var(--vd-text-muted, #57534e);
font-size: .7rem;
font-weight: 800;
}
.ux-flux-step strong {
overflow: hidden;
font-size: .78rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.ux-flux-step small {
display: none;
}
.ux-flux-step-state {
grid-column: 1 / -1;
margin-top: 5px;
color: var(--vd-text-muted, #78716c);
font-size: .65rem;
font-weight: 750;
text-transform: uppercase;
}
.ux-flux-step.is-current {
border-color: color-mix(in srgb, var(--vd-accent, #d85f35) 48%, transparent);
background: color-mix(in srgb, var(--vd-accent, #d85f35) 11%, var(--vd-surface, #fff));
}
.ux-flux-step.is-current .ux-flux-step-index {
background: var(--vd-accent, #d85f35);
color: #fff;
}
.ux-flux-step.is-complete:not(.is-current) .ux-flux-step-index {
background: color-mix(in srgb, var(--vd-success, #2f8a57) 18%, transparent);
color: var(--vd-success, #257347);
}
.ux-flux-next {
white-space: nowrap;
}
.ux-flux-system-panel {
margin-bottom: var(--ux-flux-gap);
border: 1px solid var(--vd-border, rgba(28, 25, 23, .12));
border-radius: var(--vd-radius-lg, 16px);
background: var(--vd-surface, #fff);
overflow: clip;
}
.ux-flux-system-panel > summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 17px;
cursor: pointer;
list-style: none;
}
.ux-flux-system-panel > summary::-webkit-details-marker,
.ux-flux-advanced > summary::-webkit-details-marker,
.ux-flux-preview-more > summary::-webkit-details-marker {
display: none;
}
.ux-flux-system-summary-copy {
display: grid;
gap: 2px;
}
.ux-flux-system-summary-copy small {
color: var(--vd-text-muted, #78716c);
font-size: .76rem;
}
.ux-flux-system-body {
display: grid;
gap: 14px;
padding: 0 17px 17px;
border-top: 1px solid var(--vd-border, rgba(28, 25, 23, .08));
}
.ux-flux-system-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding-top: 14px;
}
.ux-flux-system-body .flux-runtime-card {
margin: 0;
box-shadow: none;
}
#flux-studio.ux-flux-studio-ready .flux-studio-pro-layout {
grid-template-columns: minmax(0, 1.15fr) minmax(360px, .85fr);
align-items: start;
}
#flux-studio.ux-flux-studio-ready .flux-studio-left-stack,
#flux-studio.ux-flux-studio-ready .flux-studio-side-stack {
min-width: 0;
}
#flux-studio.ux-flux-studio-ready .flux-studio-controls {
display: grid;
gap: 16px;
}
#flux-studio.ux-flux-studio-ready .flux-studio-prompt-field textarea {
min-height: 150px;
resize: vertical;
}
#flux-studio.ux-flux-studio-ready .flux-profile-strip {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
#flux-studio.ux-flux-studio-ready .flux-profile-strip button {
min-width: 0;
}
.ux-flux-advanced {
border: 1px solid var(--vd-border, rgba(28, 25, 23, .1));
border-radius: var(--vd-radius-md, 12px);
background: var(--vd-surface-muted, rgba(120, 113, 108, .05));
}
.ux-flux-advanced > summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 13px 14px;
cursor: pointer;
list-style: none;
}
.ux-flux-advanced > summary > span:first-child {
display: grid;
gap: 2px;
}
.ux-flux-advanced > summary small,
.ux-flux-advanced-hint {
color: var(--vd-text-muted, #78716c);
font-size: .72rem;
}
.ux-flux-advanced-hint {
padding: 3px 8px;
border-radius: 999px;
background: var(--vd-surface-strong, #eee9e4);
font-weight: 700;
}
.ux-flux-advanced-body {
display: grid;
gap: 13px;
padding: 0 14px 14px;
}
.ux-flux-advanced-body .flux-lock-toggle {
margin: 0;
}
#flux-studio.ux-flux-studio-ready .flux-studio-preview-card {
position: sticky;
top: 14px;
}
#flux-studio.ux-flux-studio-ready .flux-studio-preview {
min-height: clamp(320px, 42vw, 560px);
}
#flux-studio.ux-flux-studio-ready .flux-studio-preview-actions {
display: grid;
gap: 9px;
}
.ux-flux-preview-primary {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.ux-flux-preview-primary .vd-button {
justify-content: center;
}
.ux-flux-preview-more {
border-top: 1px solid var(--vd-border, rgba(28, 25, 23, .09));
padding-top: 8px;
}
.ux-flux-preview-more > summary {
padding: 7px 4px;
color: var(--vd-text-muted, #78716c);
font-size: .78rem;
font-weight: 700;
cursor: pointer;
list-style: none;
}
.ux-flux-preview-more-body {
display: flex;
flex-wrap: wrap;
gap: 7px;
padding-top: 6px;
}
.ux-flux-job-quick-filters {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin: 0 0 13px;
padding: 0 2px;
}
.ux-flux-job-quick-filters .vd-button.is-active {
border-color: color-mix(in srgb, var(--vd-accent, #d85f35) 44%, transparent);
background: color-mix(in srgb, var(--vd-accent, #d85f35) 11%, var(--vd-surface, #fff));
color: var(--vd-accent, #b84b26);
}
#flux-studio.ux-flux-studio-ready .flux-job-center,
#flux-studio.ux-flux-studio-ready .flux-studio-history-section {
margin-top: 0;
}
#flux-studio.ux-flux-studio-ready .flux-job-center-actions {
align-items: center;
}
#flux-studio.ux-flux-studio-ready .flux-job-bulkbar {
transition: opacity .16s ease;
}
#flux-studio.ux-flux-studio-ready .flux-job-bulkbar:not(:has(input:checked)) > div button:disabled {
opacity: .46;
}
#flux-studio.ux-flux-studio-ready .flux-studio-history {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 12px;
}
#flux-studio.ux-flux-studio-ready .flux-history-card {
min-width: 0;
}
.ux-flux-panel-hidden {
display: none !important;
}
.ux-flux-view-tab:focus-visible,
.ux-flux-step:focus-visible,
.ux-flux-system-panel > summary:focus-visible,
.ux-flux-advanced > summary:focus-visible,
.ux-flux-preview-more > summary:focus-visible,
.ux-flux-job-quick-filters button:focus-visible {
outline: 3px solid color-mix(in srgb, var(--vd-accent, #d85f35) 45%, transparent);
outline-offset: 2px;
}
@media (max-width: 1180px) {
.ux-flux-workflow {
grid-template-columns: 1fr;
}
.ux-flux-next {
justify-self: start;
}
#flux-studio.ux-flux-studio-ready .flux-studio-pro-layout {
grid-template-columns: 1fr;
}
#flux-studio.ux-flux-studio-ready .flux-studio-preview-card {
position: static;
}
}
@media (max-width: 760px) {
.ux-flux-view-navigation {
grid-template-columns: 1fr;
}
.ux-flux-view-tab {
padding: 11px 12px;
}
.ux-flux-steps {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#flux-studio.ux-flux-studio-ready .flux-profile-strip {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ux-flux-preview-primary {
grid-template-columns: 1fr;
}
#flux-studio.ux-flux-studio-ready .flux-job-center-head,
#flux-studio.ux-flux-studio-ready .flux-studio-history-head {
align-items: stretch;
}
#flux-studio.ux-flux-studio-ready .flux-job-center-actions,
#flux-studio.ux-flux-studio-ready .flux-library-actions {
width: 100%;
}
}
@media (max-width: 470px) {
.ux-flux-workflow {
padding: 14px;
}
.ux-flux-steps,
#flux-studio.ux-flux-studio-ready .flux-profile-strip {
grid-template-columns: 1fr;
}
.ux-flux-step {
grid-template-columns: auto 1fr auto;
align-items: center;
}
.ux-flux-step-state {
grid-column: 3;
grid-row: 1 / span 2;
margin: 0;
}
.ux-flux-system-panel > summary {
align-items: flex-start;
flex-direction: column;
}
.ux-flux-system-actions,
.ux-flux-preview-more-body,
.ux-flux-job-quick-filters {
display: grid;
grid-template-columns: 1fr;
}
.ux-flux-system-actions .vd-button,
.ux-flux-preview-more-body .vd-button,
.ux-flux-preview-more-body a,
.ux-flux-job-quick-filters .vd-button {
width: 100%;
justify-content: center;
}
}
@media (prefers-reduced-motion: reduce) {
.ux-flux-view-tab,
#flux-studio.ux-flux-studio-ready .flux-job-bulkbar {
transition: none;
}
#flux-studio.ux-flux-studio-ready * {
scroll-behavior: auto !important;
}
}
+478
View File
@@ -0,0 +1,478 @@
(() => {
'use strict';
const ROOT_ID = 'flux-studio';
const VIEW_KEY = 'vendoo_flux_studio_view';
const VALID_VIEWS = ['create', 'jobs', 'library'];
const createNode = (tag, className = '', text = '') => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text) node.textContent = text;
return node;
};
const setText = (node, value) => {
if (!node) return;
const normalized = String(value ?? '');
if (node.textContent !== normalized) node.textContent = normalized;
};
const setData = (node, name, value) => {
if (!node) return;
const normalized = String(value ?? '');
if (node.dataset[name] !== normalized) node.dataset[name] = normalized;
};
const readStoredView = () => {
try {
const stored = localStorage.getItem(VIEW_KEY) || 'create';
return VALID_VIEWS.includes(stored) ? stored : 'create';
} catch {
return 'create';
}
};
const storeView = view => {
try { localStorage.setItem(VIEW_KEY, view); } catch {}
};
const focusNode = node => {
if (!node) return;
node.scrollIntoView({ behavior: 'smooth', block: 'center' });
window.requestAnimationFrame(() => {
const target = node.matches('input,button,select,textarea,summary,a[href]')
? node
: node.querySelector('input,button,select,textarea,summary,a[href]');
target?.focus({ preventScroll: true });
});
};
function createViewButton(view, label, copy) {
const button = createNode('button', 'ux-flux-view-tab');
button.type = 'button';
button.dataset.uxFluxView = view;
button.setAttribute('role', 'tab');
button.setAttribute('aria-selected', 'false');
const body = createNode('span', 'ux-flux-view-copy');
body.append(createNode('strong', '', label), createNode('small', '', copy));
const badge = createNode('span', 'ux-flux-view-badge');
badge.dataset.uxFluxViewBadge = view;
button.append(body, badge);
return button;
}
function createViewNavigation(root) {
if (root.querySelector('.ux-flux-view-navigation')) return;
const header = root.querySelector('.flux-studio-heading');
const layout = root.querySelector('.flux-studio-layout');
if (!header || !layout) return;
const navigation = createNode('nav', 'ux-flux-view-navigation');
navigation.setAttribute('aria-label', 'FLUX Studio Arbeitsbereiche');
navigation.setAttribute('role', 'tablist');
navigation.append(
createViewButton('create', 'Erstellen', 'Prompt, Profil und Vorschau'),
createViewButton('jobs', 'Aufträge', 'Warteschlange, Fehler und Archiv'),
createViewButton('library', 'Bibliothek', 'Ergebnisse und Favoriten')
);
header.after(navigation);
}
function setView(root, view, { persist = true, focus = false } = {}) {
const normalized = VALID_VIEWS.includes(view) ? view : 'create';
setData(root, 'uxFluxView', normalized);
if (persist) storeView(normalized);
root.querySelectorAll('[data-ux-flux-view]').forEach(button => {
const active = button.dataset.uxFluxView === normalized;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', String(active));
button.tabIndex = active ? 0 : -1;
});
const panels = {
create: root.querySelector('.flux-studio-layout'),
jobs: root.querySelector('.flux-job-center'),
library: root.querySelector('.flux-studio-history-section'),
};
Object.entries(panels).forEach(([id, panel]) => {
if (!panel) return;
const active = id === normalized;
panel.classList.toggle('ux-flux-panel-hidden', !active);
panel.setAttribute('aria-hidden', String(!active));
});
if (focus) {
const target = normalized === 'create'
? root.querySelector('#flux-studio-prompt')
: normalized === 'jobs'
? root.querySelector('#flux-job-center-toggle, #flux-job-center-body')
: root.querySelector('#flux-studio-favorites-only, #flux-studio-history');
focusNode(target);
}
}
function createWorkflow(root) {
if (root.querySelector('.ux-flux-workflow')) return;
const navigation = root.querySelector('.ux-flux-view-navigation');
if (!navigation) return;
const workflow = createNode('section', 'ux-flux-workflow');
workflow.setAttribute('aria-label', 'Geführter FLUX-Ablauf');
const copy = createNode('div', 'ux-flux-workflow-copy');
copy.append(
createNode('span', 'ux-flux-kicker', 'Geführte Bildgenerierung'),
createNode('h3', '', 'Vom Prompt zum verwendbaren Produktbild'),
createNode('p', '', 'Die wichtigsten Schritte bleiben sichtbar. Systemdetails, Auftragsverwaltung und Bibliothek sind getrennt erreichbar.')
);
const steps = createNode('div', 'ux-flux-steps');
const definitions = [
['prompt', '1', 'Prompt', 'Motiv und Bildsprache beschreiben'],
['profile', '2', 'Profil', 'Varianten und Qualitätsziel wählen'],
['result', '3', 'Ergebnis', 'Bild prüfen und übernehmen'],
['monitor', '4', 'Betrieb', 'Aufträge und Fehler überwachen'],
];
definitions.forEach(([id, number, title, detail]) => {
const button = createNode('button', 'ux-flux-step');
button.type = 'button';
button.dataset.uxFluxStep = id;
button.setAttribute('aria-pressed', 'false');
button.append(
createNode('span', 'ux-flux-step-index', number),
createNode('strong', '', title),
createNode('small', '', detail),
createNode('span', 'ux-flux-step-state', 'Offen')
);
steps.append(button);
});
const next = createNode('button', 'vd-button primary ux-flux-next', 'Prompt eingeben');
next.type = 'button';
next.dataset.uxFluxNext = 'prompt';
workflow.append(copy, steps, next);
navigation.after(workflow);
}
function createSystemPanel(root) {
if (root.querySelector('.ux-flux-system-panel')) return;
const headingActions = root.querySelector('.flux-studio-engine-actions');
const workflow = root.querySelector('.ux-flux-workflow');
const runtime = root.querySelector('.flux-runtime-card');
if (!headingActions || !workflow || !runtime) return;
const reset = headingActions.querySelector('#flux-studio-reset');
const chip = headingActions.querySelector('#flux-studio-engine-chip');
const controls = [
headingActions.querySelector('#flux-studio-refresh-engine'),
headingActions.querySelector('#flux-studio-start-engine'),
headingActions.querySelector('#flux-studio-stop-engine'),
].filter(Boolean);
const panel = createNode('details', 'ux-flux-system-panel');
const summary = createNode('summary');
const summaryCopy = createNode('span', 'ux-flux-system-summary-copy');
summaryCopy.append(
createNode('strong', '', 'FLUX-System & ComfyUI'),
createNode('small', '', 'Engine-Status, Start/Stop und Laufzeitdaten')
);
if (chip) summary.append(summaryCopy, chip);
else summary.append(summaryCopy);
const body = createNode('div', 'ux-flux-system-body');
const actionBar = createNode('div', 'ux-flux-system-actions');
controls.forEach(button => actionBar.append(button));
body.append(actionBar, runtime);
panel.append(summary, body);
workflow.after(panel);
headingActions.replaceChildren();
if (reset) headingActions.append(reset);
}
function groupAdvancedControls(root) {
const controls = root.querySelector('.flux-studio-controls');
if (!controls || controls.querySelector('.ux-flux-advanced')) return;
const fields = controls.querySelector('.flux-studio-fields');
const lock = controls.querySelector('.flux-lock-toggle');
const runRow = controls.querySelector('.flux-studio-run-row');
if (!fields || !runRow) return;
const advanced = createNode('details', 'ux-flux-advanced');
const summary = createNode('summary');
const copy = createNode('span');
copy.append(
createNode('strong', '', 'Erweiterte Einstellungen'),
createNode('small', '', 'Prompt-Modus, Stil, Format, Schritte und Seed')
);
summary.append(copy, createNode('span', 'ux-flux-advanced-hint', 'Optional'));
const body = createNode('div', 'ux-flux-advanced-body');
if (lock) body.append(lock);
body.append(fields);
advanced.append(summary, body);
runRow.before(advanced);
}
function simplifyPreviewActions(root) {
const actions = root.querySelector('#flux-studio-preview-actions');
if (!actions || actions.dataset.uxFluxStructured === 'true') return;
const primary = createNode('div', 'ux-flux-preview-primary');
const more = createNode('details', 'ux-flux-preview-more');
const summary = createNode('summary', '', 'Weitere Bildaktionen');
const moreBody = createNode('div', 'ux-flux-preview-more-body');
const edit = actions.querySelector('#flux-studio-edit');
const use = actions.querySelector('#flux-studio-use-in-generator');
const similar = actions.querySelector('#flux-studio-similar');
const favorite = actions.querySelector('#flux-studio-favorite');
const download = actions.querySelector('#flux-studio-download');
const remove = actions.querySelector('#flux-studio-delete');
if (use) primary.append(use);
if (edit) primary.append(edit);
[similar, favorite, download, remove].filter(Boolean).forEach(node => moreBody.append(node));
more.append(summary, moreBody);
actions.append(primary, more);
actions.dataset.uxFluxStructured = 'true';
}
function createJobQuickFilters(root) {
const center = root.querySelector('.flux-job-center');
const select = root.querySelector('#flux-job-filter');
if (!center || !select || center.querySelector('.ux-flux-job-quick-filters')) return;
const filters = createNode('div', 'ux-flux-job-quick-filters');
filters.setAttribute('role', 'group');
filters.setAttribute('aria-label', 'FLUX-Aufträge schnell filtern');
const definitions = [
['', 'Alle'],
['active', 'Aktiv'],
['completed', 'Erfolgreich'],
['failed', 'Fehler'],
['cancelled', 'Abgebrochen'],
];
definitions.forEach(([value, label]) => {
const button = createNode('button', 'vd-button compact', label);
button.type = 'button';
button.dataset.uxFluxJobFilter = value || 'all';
button.addEventListener('click', () => {
select.value = value;
select.dispatchEvent(new Event('change', { bubbles: true }));
updateJobFilters(root);
});
filters.append(button);
});
const head = center.querySelector('.flux-job-center-head');
head?.after(filters);
}
function updateJobFilters(root) {
const value = root.querySelector('#flux-job-filter')?.value || '';
root.querySelectorAll('[data-ux-flux-job-filter]').forEach(button => {
const active = button.dataset.uxFluxJobFilter === (value || 'all');
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', String(active));
});
}
function readJobState(root) {
const summary = root.querySelector('#flux-job-summary');
const text = (summary?.textContent || '').toLowerCase();
const listText = (root.querySelector('#flux-job-list')?.textContent || '').toLowerCase();
const combined = `${text} ${listText}`;
const active = /wartende varianten\s*[1-9]|laufende varianten\s*[1-9]|wartend|läuft|running|queued|cancelling/.test(combined);
const issues = /fehlgeschlagen|fehler|failed|teilweise fertig|completed_with_errors/.test(combined);
const hasJobs = Boolean(root.querySelector('#flux-job-list')?.children.length) && !/keine aufträge|werden geladen/.test(listText);
return { active, issues, hasJobs };
}
function readLibraryCount(root) {
const info = root.querySelector('#flux-history-page-info')?.textContent || '';
const explicit = info.match(/·\s*(\d+)\s+Bilder?/i);
if (explicit) return Number(explicit[1]);
const history = root.querySelector('#flux-studio-history');
if (!history || /keine flux-bilder|wird geladen/.test((history.textContent || '').toLowerCase())) return 0;
return [...history.children].filter(node => !node.classList.contains('empty-state')).length;
}
function updateBadges(root) {
const jobState = readJobState(root);
const jobBadge = root.querySelector('[data-ux-flux-view-badge="jobs"]');
if (jobBadge) {
setText(jobBadge, jobState.issues ? 'Problem' : jobState.active ? 'Aktiv' : jobState.hasJobs ? 'Verlauf' : '');
setData(jobBadge, 'tone', jobState.issues ? 'danger' : jobState.active ? 'working' : 'neutral');
}
const libraryBadge = root.querySelector('[data-ux-flux-view-badge="library"]');
if (libraryBadge) {
const count = readLibraryCount(root);
setText(libraryBadge, count ? String(count) : '');
setData(libraryBadge, 'tone', 'neutral');
}
}
function updateWorkflow(root) {
const prompt = root.querySelector('#flux-studio-prompt')?.value.trim() || '';
const profile = root.querySelector('[data-flux-profile].active') || root.querySelector('[data-flux-profile="balanced"]');
const variants = root.querySelector('[data-flux-variants].active') || root.querySelector('[data-flux-variants="1"]');
const image = root.querySelector('#flux-studio-preview-image');
const hasResult = Boolean(image?.getAttribute('src')) && !image.classList.contains('hidden');
const progress = root.querySelector('#flux-studio-progress');
const isBusy = Boolean(progress && !progress.classList.contains('hidden'));
const jobState = readJobState(root);
const completed = {
prompt: prompt.length >= 3,
profile: Boolean(profile && variants),
result: hasResult,
monitor: jobState.hasJobs && !jobState.active && !jobState.issues,
};
const current = !completed.prompt
? 'prompt'
: jobState.issues || jobState.active || isBusy
? 'monitor'
: hasResult
? 'result'
: 'profile';
setData(root, 'uxFluxCurrentStep', current);
root.querySelectorAll('[data-ux-flux-step]').forEach(step => {
const id = step.dataset.uxFluxStep;
const active = id === current;
step.classList.toggle('is-current', active);
step.classList.toggle('is-complete', Boolean(completed[id]));
step.setAttribute('aria-pressed', String(active));
setText(step.querySelector('.ux-flux-step-state'), completed[id] ? 'Erledigt' : active ? 'Jetzt' : 'Offen');
});
const next = root.querySelector('[data-ux-flux-next]');
if (next) {
if (!completed.prompt) {
setData(next, 'uxFluxNext', 'prompt');
setText(next, 'Prompt eingeben');
} else if (jobState.issues || jobState.active || isBusy) {
setData(next, 'uxFluxNext', 'jobs');
setText(next, jobState.issues ? 'Probleme prüfen' : 'Auftrag überwachen');
} else if (hasResult) {
setData(next, 'uxFluxNext', 'result');
setText(next, 'Ergebnis prüfen');
} else {
setData(next, 'uxFluxNext', 'generate');
setText(next, 'Profil prüfen & Auftrag starten');
}
}
updateBadges(root);
updateJobFilters(root);
}
function handleStep(root, step) {
if (step === 'jobs' || step === 'monitor') {
setView(root, 'jobs', { focus: true });
return;
}
if (step === 'library') {
setView(root, 'library', { focus: true });
return;
}
setView(root, 'create');
const targets = {
prompt: root.querySelector('#flux-studio-prompt'),
profile: root.querySelector('.flux-profile-strip'),
generate: root.querySelector('#flux-studio-generate'),
result: root.querySelector('.flux-studio-preview-card'),
};
focusNode(targets[step] || targets.prompt);
}
function bindInteractions(root) {
root.querySelectorAll('[data-ux-flux-view]').forEach(button => {
button.addEventListener('click', () => setView(root, button.dataset.uxFluxView, { focus: true }));
button.addEventListener('keydown', event => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
event.preventDefault();
const tabs = [...root.querySelectorAll('[data-ux-flux-view]')];
const index = tabs.indexOf(button);
const nextIndex = event.key === 'Home' ? 0
: event.key === 'End' ? tabs.length - 1
: event.key === 'ArrowRight' ? (index + 1) % tabs.length
: (index - 1 + tabs.length) % tabs.length;
tabs[nextIndex]?.focus();
tabs[nextIndex]?.click();
});
});
root.querySelectorAll('[data-ux-flux-step]').forEach(button => {
button.addEventListener('click', () => handleStep(root, button.dataset.uxFluxStep));
});
root.querySelector('[data-ux-flux-next]')?.addEventListener('click', event => {
handleStep(root, event.currentTarget.dataset.uxFluxNext);
});
root.querySelector('#flux-job-filter')?.addEventListener('change', () => updateJobFilters(root));
}
function observeState(root) {
let scheduled = false;
const schedule = () => {
if (scheduled) return;
scheduled = true;
window.requestAnimationFrame(() => {
scheduled = false;
updateWorkflow(root);
});
};
const observer = new MutationObserver(schedule);
const targets = [
root.querySelector('#flux-job-summary'),
root.querySelector('#flux-job-list'),
root.querySelector('#flux-history-page-info'),
root.querySelector('#flux-studio-history'),
root.querySelector('#flux-studio-preview-image'),
root.querySelector('#flux-studio-progress'),
root.querySelector('#flux-studio-status'),
].filter(Boolean);
targets.forEach(target => observer.observe(target, {
subtree: true,
childList: true,
characterData: true,
attributes: true,
attributeFilter: ['class', 'src', 'data-state', 'data-tone', 'disabled'],
}));
root.querySelectorAll('input,textarea,select').forEach(field => {
field.addEventListener('input', schedule);
field.addEventListener('change', schedule);
});
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
function initializeFluxStudioUx() {
const root = document.getElementById(ROOT_ID);
if (!root || root.classList.contains('ux-flux-studio-ready')) return;
createViewNavigation(root);
createWorkflow(root);
createSystemPanel(root);
groupAdvancedControls(root);
simplifyPreviewActions(root);
createJobQuickFilters(root);
bindInteractions(root);
setView(root, readStoredView(), { persist: false });
updateWorkflow(root);
observeState(root);
root.classList.add('ux-flux-studio-ready');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeFluxStudioUx, { once: true });
} else {
initializeFluxStudioUx();
}
})();
+459
View File
@@ -0,0 +1,459 @@
/* Vendoo Generator Workflow 1.44.0
* Geführter Drei-Schritt-Ablauf für neue Artikel.
*/
.ux-generator-guided .generator-heading {
align-items: center;
}
.ux-generator-flow {
background: linear-gradient(135deg, var(--bg-card), var(--bg-page));
border: 1px solid var(--border);
border-radius: var(--vd-radius-xl);
box-shadow: var(--vd-shadow-soft);
margin-bottom: 18px;
overflow: hidden;
}
.ux-generator-flow-head {
align-items: start;
display: grid;
gap: 22px;
grid-template-columns: minmax(0, 1fr) minmax(220px, 320px);
padding: 22px 24px 17px;
}
.ux-generator-flow-kicker {
color: var(--accent);
display: block;
font-size: 10px;
font-weight: 800;
letter-spacing: .1em;
margin-bottom: 7px;
text-transform: uppercase;
}
.ux-generator-flow-head h3 {
font-size: clamp(20px, 2vw, 27px);
letter-spacing: -.035em;
line-height: 1.15;
margin: 0 0 7px;
}
.ux-generator-flow-head p {
color: var(--text-secondary);
line-height: 1.55;
margin: 0;
max-width: 720px;
}
.ux-generator-overall {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--vd-radius-lg);
display: flex;
flex-direction: column;
gap: 4px;
min-height: 76px;
padding: 14px 16px;
}
.ux-generator-overall strong {
color: var(--text);
font-size: 14px;
}
.ux-generator-overall span {
color: var(--text-secondary);
font-size: 12px;
line-height: 1.45;
}
.ux-generator-progress {
background: color-mix(in srgb, var(--border) 72%, transparent);
height: 4px;
overflow: hidden;
}
.ux-generator-progress span {
background: linear-gradient(90deg, var(--accent), var(--success));
display: block;
height: 100%;
transition: width .28s ease;
width: 0;
}
.ux-generator-stages {
display: grid;
gap: 1px;
grid-template-columns: repeat(3, minmax(0, 1fr));
background: var(--border);
border-bottom: 1px solid var(--border);
}
.ux-generator-stage {
align-items: center;
background: var(--bg-card);
border: 0;
color: var(--text-secondary);
cursor: pointer;
display: grid;
font: inherit;
gap: 11px;
grid-template-columns: auto minmax(0, 1fr) auto;
min-height: 84px;
padding: 14px 16px;
text-align: left;
transition: background .16s ease, color .16s ease;
}
.ux-generator-stage:hover {
background: var(--bg-hover);
color: var(--text);
}
.ux-generator-stage.is-current {
background: var(--accent-light);
color: var(--text);
box-shadow: inset 0 -3px 0 var(--accent);
}
.ux-generator-stage.is-complete {
color: var(--text);
}
.ux-generator-stage-number {
align-items: center;
background: var(--bg-page);
border: 1px solid var(--border);
border-radius: 999px;
display: inline-flex;
font-size: 12px;
font-weight: 800;
height: 30px;
justify-content: center;
width: 30px;
}
.ux-generator-stage.is-current .ux-generator-stage-number {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.ux-generator-stage.is-complete .ux-generator-stage-number {
background: var(--success);
border-color: var(--success);
color: white;
}
.ux-generator-stage > span:nth-child(2) {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.ux-generator-stage strong {
color: inherit;
font-size: 13px;
}
.ux-generator-stage small {
color: var(--text-muted);
font-size: 11px;
line-height: 1.35;
}
.ux-generator-stage-state {
background: var(--bg-page);
border-radius: 999px;
color: var(--text-muted);
font-size: 10px;
font-weight: 750;
padding: 5px 8px;
white-space: nowrap;
}
.ux-generator-stage.is-current .ux-generator-stage-state {
background: var(--accent);
color: white;
}
.ux-generator-stage.is-complete .ux-generator-stage-state {
background: var(--vd-success-soft);
color: var(--success);
}
.ux-generator-next {
align-items: center;
display: flex;
gap: 18px;
justify-content: space-between;
padding: 15px 18px;
}
.ux-generator-next-copy {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.ux-generator-next-copy > span {
color: var(--text-muted);
font-size: 10px;
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
}
.ux-generator-next-copy strong {
color: var(--text);
font-size: 14px;
}
.ux-generator-next-copy small {
color: var(--text-secondary);
line-height: 1.4;
}
.ux-generator-guided .generator-commandbar {
backdrop-filter: blur(12px);
background: color-mix(in srgb, var(--bg-card) 92%, transparent);
border: 1px solid var(--border);
border-radius: var(--vd-radius-lg);
box-shadow: var(--vd-shadow-soft);
position: sticky;
top: 76px;
z-index: 45;
}
.ux-generator-guided .generator-studio-layout {
align-items: start;
display: grid;
gap: 18px;
grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr);
}
.ux-generator-guided .generator-preview-panel {
grid-column: 1 / -1;
}
.ux-generator-guided .generator-panel {
border-radius: var(--vd-radius-xl);
min-width: 0;
scroll-margin-top: 150px;
transition: border-color .18s ease, box-shadow .18s ease;
}
.ux-generator-guided .generator-panel.ux-generator-panel-focus {
border-color: var(--accent);
box-shadow: 0 0 0 4px var(--accent-light), var(--vd-shadow-soft);
}
.ux-generator-guided .generator-config-stack {
gap: 12px;
}
.ux-generator-guided .generator-config-card,
.ux-generator-guided .generator-notes-card {
border-radius: var(--vd-radius-lg);
}
.ux-generator-guided .generator-config-card.ux-generator-essential {
border-color: color-mix(in srgb, var(--accent) 20%, var(--border));
}
.ux-generator-advanced,
.ux-generator-optional {
border: 1px solid var(--border);
border-radius: var(--vd-radius-lg);
overflow: clip;
}
.ux-generator-advanced > summary,
.ux-generator-optional > summary {
align-items: center;
background: var(--bg-page);
cursor: pointer;
display: flex;
gap: 14px;
justify-content: space-between;
list-style: none;
min-height: 64px;
padding: 13px 15px;
}
.ux-generator-advanced > summary::-webkit-details-marker,
.ux-generator-optional > summary::-webkit-details-marker {
display: none;
}
.ux-generator-advanced > summary > span,
.ux-generator-optional > summary > span {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.ux-generator-advanced > summary strong,
.ux-generator-optional > summary strong {
color: var(--text);
font-size: 13px;
}
.ux-generator-advanced > summary small,
.ux-generator-optional > summary small {
color: var(--text-muted);
font-size: 11px;
line-height: 1.4;
}
.ux-generator-advanced > summary b,
.ux-generator-optional > summary b {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 999px;
color: var(--text-secondary);
font-size: 10px;
font-weight: 750;
max-width: 48%;
overflow: hidden;
padding: 5px 8px;
text-overflow: ellipsis;
white-space: nowrap;
}
.ux-generator-advanced > summary::after,
.ux-generator-optional > summary::after {
color: var(--text-muted);
content: '';
flex: 0 0 auto;
font-size: 18px;
margin-left: -5px;
transform: rotate(90deg);
transition: transform .16s ease;
}
.ux-generator-advanced:not([open]) > summary::after,
.ux-generator-optional:not([open]) > summary::after {
transform: rotate(0deg);
}
.ux-generator-advanced-content,
.ux-generator-optional-content {
background: var(--bg-card);
display: grid;
gap: 12px;
padding: 13px;
}
.ux-generator-advanced-content > .generator-config-card,
.ux-generator-advanced-content > .generator-config-pair,
.ux-generator-advanced-content > .generator-notes-card {
margin: 0;
}
.ux-generator-guided .generator-ai-model-card,
.ux-generator-guided .generator-photo-quality {
border: 0;
margin: 0;
}
.ux-generator-guided .generator-photo-strip:empty {
display: none;
}
.ux-generator-guided .generator-preview-panel .generator-result {
margin-inline: auto;
max-width: 1180px;
}
.ux-generator-guided .generator-preview-empty {
min-height: 320px;
}
.ux-generator-guided .generator-html-details:not([open]) {
background: var(--bg-page);
}
.ux-generator-guided .generator-html-details > summary {
min-height: 54px;
}
.ux-generator-guided [data-ux-generator-stage]:focus-visible,
.ux-generator-guided [data-ux-generator-action]:focus-visible,
.ux-generator-guided .ux-generator-advanced > summary:focus-visible,
.ux-generator-guided .ux-generator-optional > summary:focus-visible,
.ux-generator-guided #dropzone:focus-visible {
outline: 3px solid var(--accent-light);
outline-offset: 2px;
}
@media (max-width: 1100px) {
.ux-generator-flow-head {
grid-template-columns: 1fr;
}
.ux-generator-overall {
min-height: auto;
}
.ux-generator-guided .generator-studio-layout {
grid-template-columns: 1fr;
}
.ux-generator-guided .generator-preview-panel {
grid-column: auto;
}
}
@media (max-width: 760px) {
.ux-generator-flow-head {
padding: 18px;
}
.ux-generator-stages {
grid-template-columns: 1fr;
}
.ux-generator-stage {
min-height: 70px;
}
.ux-generator-next {
align-items: stretch;
flex-direction: column;
}
.ux-generator-next .vd-button {
justify-content: center;
width: 100%;
}
.ux-generator-guided .generator-commandbar {
position: static;
}
.ux-generator-advanced > summary,
.ux-generator-optional > summary {
align-items: flex-start;
}
.ux-generator-advanced > summary b,
.ux-generator-optional > summary b {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.ux-generator-progress span,
.ux-generator-stage,
.ux-generator-panel,
.ux-generator-advanced > summary::after,
.ux-generator-optional > summary::after {
transition: none;
}
}
+387
View File
@@ -0,0 +1,387 @@
(() => {
'use strict';
const root = document.getElementById('generator');
if (!root || root.dataset.uxGeneratorWorkflow === 'ready') return;
root.dataset.uxGeneratorWorkflow = 'ready';
root.classList.add('ux-generator-guided');
const STORAGE = {
advanced: 'vendoo_ux_generator_advanced_open',
photoHelp: 'vendoo_ux_generator_photo_help_open',
imageStudio: 'vendoo_ux_generator_image_studio_open',
htmlPreview: 'vendoo_ux_generator_html_preview_open',
};
const stageTargets = {
photos: '.media-workbench',
settings: '.generator-config-panel',
result: '.generator-preview-panel',
};
let flow = null;
let updateScheduled = false;
function setText(element, value) {
if (element && element.textContent !== value) element.textContent = value;
}
function isVisible(element) {
return !!element && !element.classList.contains('hidden') && !element.hidden;
}
function createFlow() {
if (root.querySelector('.ux-generator-flow')) return root.querySelector('.ux-generator-flow');
const commandbar = document.getElementById('generator-commandbar');
if (!commandbar) return null;
const element = document.createElement('section');
element.className = 'ux-generator-flow';
element.setAttribute('aria-label', 'Geführter Artikelablauf');
element.innerHTML = `
<div class="ux-generator-flow-head">
<div>
<span class="ux-generator-flow-kicker">Geführter Ablauf</span>
<h3>In drei klaren Schritten zum fertigen Artikel</h3>
<p>Vendoo zeigt nur das, was du im aktuellen Schritt wirklich benötigst. Erweiterte Optionen bleiben jederzeit erreichbar.</p>
</div>
<div class="ux-generator-overall" aria-live="polite">
<strong id="ux-generator-progress-label">Noch nicht begonnen</strong>
<span id="ux-generator-progress-copy">Starte mit mindestens einem Produktfoto.</span>
</div>
</div>
<div class="ux-generator-progress" aria-hidden="true"><span id="ux-generator-progress-bar"></span></div>
<div class="ux-generator-stages" role="list">
<button type="button" class="ux-generator-stage is-current" data-ux-generator-stage="photos" role="listitem">
<span class="ux-generator-stage-number">1</span>
<span><strong>Fotos hinzufügen</strong><small>Produkt gut erkennbar aufnehmen</small></span>
<b class="ux-generator-stage-state" data-ux-stage-state="photos">Offen</b>
</button>
<button type="button" class="ux-generator-stage" data-ux-generator-stage="settings" role="listitem">
<span class="ux-generator-stage-number">2</span>
<span><strong>Ziel festlegen</strong><small>Plattform und optional Vorlage wählen</small></span>
<b class="ux-generator-stage-state" data-ux-stage-state="settings">Danach</b>
</button>
<button type="button" class="ux-generator-stage" data-ux-generator-stage="result" role="listitem">
<span class="ux-generator-stage-number">3</span>
<span><strong>Prüfen & speichern</strong><small>AI-Ergebnis kontrollieren und ergänzen</small></span>
<b class="ux-generator-stage-state" data-ux-stage-state="result">Danach</b>
</button>
</div>
<div class="ux-generator-next">
<div class="ux-generator-next-copy">
<span>Nächster sinnvoller Schritt</span>
<strong id="ux-generator-next-title">Produktfoto hinzufügen</strong>
<small id="ux-generator-next-copy">Ein Foto genügt zum Start; drei oder mehr Perspektiven verbessern die Erkennung.</small>
</div>
<button type="button" class="vd-button primary" id="ux-generator-next-button" data-ux-generator-action="focus-photos">Fotos hinzufügen</button>
</div>`;
commandbar.before(element);
return element;
}
function createAdvancedSettings() {
const stack = root.querySelector('.generator-config-stack');
if (!stack || stack.querySelector('.ux-generator-advanced')) return;
const platform = document.getElementById('platform-buttons')?.closest('.generator-config-card');
const template = document.getElementById('template-select')?.closest('.generator-config-card');
const advancedItems = [
document.getElementById('provider-buttons')?.closest('.generator-config-card'),
document.getElementById('model-select-wrap'),
root.querySelector('.generator-config-pair'),
root.querySelector('.generator-notes-card'),
].filter(Boolean);
if (!advancedItems.length) return;
platform?.classList.add('ux-generator-essential');
template?.classList.add('ux-generator-essential');
const details = document.createElement('details');
details.className = 'ux-generator-advanced';
details.open = localStorage.getItem(STORAGE.advanced) === 'true';
details.innerHTML = `
<summary>
<span><strong>Weitere Einstellungen</strong><small>AI-Dienst, Modell, Sprache, Varianten und Artikelhinweise</small></span>
<b id="ux-generator-advanced-summary">Standardwerte aktiv</b>
</summary>
<div class="ux-generator-advanced-content"></div>`;
const content = details.querySelector('.ux-generator-advanced-content');
advancedItems.forEach(item => content.append(item));
details.addEventListener('toggle', () => localStorage.setItem(STORAGE.advanced, String(details.open)));
if (template) template.after(details);
else stack.append(details);
}
function wrapOptionalSection(element, options) {
if (!element || element.closest('.ux-generator-optional')) return null;
const details = document.createElement('details');
details.className = `ux-generator-optional ${options.className || ''}`.trim();
details.open = localStorage.getItem(options.storageKey) === 'true';
const summary = document.createElement('summary');
summary.innerHTML = `<span><strong>${options.title}</strong><small>${options.copy}</small></span><b>${options.badge || 'Optional'}</b>`;
const content = document.createElement('div');
content.className = 'ux-generator-optional-content';
element.before(details);
content.append(element);
details.append(summary, content);
details.addEventListener('toggle', () => localStorage.setItem(options.storageKey, String(details.open)));
return details;
}
function simplifyMediaPanel() {
const imageStudio = document.getElementById('generator-ai-model-card');
const photoHelp = root.querySelector('.generator-photo-quality');
const studioDetails = wrapOptionalSection(imageStudio, {
title: 'Produktbild Studio',
copy: 'Zusätzliche AI-Bildvarianten erzeugen',
badge: 'Optional',
className: 'ux-generator-image-studio',
storageKey: STORAGE.imageStudio,
});
const helpDetails = wrapOptionalSection(photoHelp, {
title: 'Tipps für bessere Fotos',
copy: 'Qualitätscheck und empfohlene Perspektiven',
badge: 'Hilfe',
className: 'ux-generator-photo-help',
storageKey: STORAGE.photoHelp,
});
if (studioDetails && imageStudio) studioDetails.hidden = imageStudio.classList.contains('hidden');
if (helpDetails) helpDetails.open = localStorage.getItem(STORAGE.photoHelp) === 'true';
}
function simplifyResultPanel() {
const htmlDetails = document.getElementById('html-preview-wrap');
if (!htmlDetails) return;
htmlDetails.open = localStorage.getItem(STORAGE.htmlPreview) === 'true';
htmlDetails.addEventListener('toggle', () => localStorage.setItem(STORAGE.htmlPreview, String(htmlDetails.open)));
const summary = htmlDetails.querySelector('summary');
if (summary) summary.firstChild.textContent = 'Plattform- und HTML-Vorschau ';
}
function selectedPlatform() {
const container = document.getElementById('platform-buttons');
return container?.querySelector('.active, [aria-pressed="true"], [data-selected="true"]') || null;
}
function getPhotoCount() {
const previewCount = document.getElementById('photo-preview')?.children.length || 0;
const countLabel = document.getElementById('generator-photo-count')?.textContent || '';
const parsed = Number.parseInt(countLabel.match(/\d+/)?.[0] || '0', 10);
return Math.max(previewCount, Number.isFinite(parsed) ? parsed : 0);
}
function getWorkflowState() {
const photoCount = getPhotoCount();
const platform = selectedPlatform();
const loading = isVisible(document.getElementById('loading'));
const result = isVisible(document.getElementById('result-section'));
const generate = document.getElementById('generate-btn');
const save = document.getElementById('save-btn');
return {
photoCount,
hasPhotos: photoCount > 0,
hasPlatform: !!platform,
platformLabel: platform?.textContent?.trim() || '',
loading,
hasResult: result,
canGenerate: !!generate && !generate.disabled,
canSave: !!save && !save.disabled,
};
}
function stageStatus(state) {
if (!state.hasPhotos) return { stage: 'photos', progress: 0 };
if (!state.hasPlatform) return { stage: 'settings', progress: 34 };
if (state.loading || !state.hasResult) return { stage: 'settings', progress: state.loading ? 58 : 50 };
if (state.hasResult && !state.canSave) return { stage: 'result', progress: 78 };
return { stage: 'result', progress: 100 };
}
function updateOptionalVisibility() {
const imageStudio = document.getElementById('generator-ai-model-card');
const wrapper = root.querySelector('.ux-generator-image-studio');
if (wrapper && imageStudio) wrapper.hidden = imageStudio.classList.contains('hidden');
}
function updateAdvancedSummary(state) {
const summary = document.getElementById('ux-generator-advanced-summary');
if (!summary) return;
const provider = document.getElementById('provider-buttons')?.querySelector('.active, [aria-pressed="true"]')?.textContent?.trim();
const language = document.getElementById('language-select')?.selectedOptions?.[0]?.textContent?.trim();
const variants = document.getElementById('variants-select')?.value || '1';
const parts = [provider, language, `${variants} Variante${variants === '1' ? '' : 'n'}`].filter(Boolean);
setText(summary, parts.length ? parts.join(' · ') : (state.hasPlatform ? 'Standardwerte aktiv' : 'Nach Plattformwahl verfügbar'));
}
function updateWorkflow() {
updateScheduled = false;
if (!flow) flow = createFlow();
if (!flow) return;
const state = getWorkflowState();
const status = stageStatus(state);
root.dataset.uxGeneratorStage = status.stage;
const bar = document.getElementById('ux-generator-progress-bar');
if (bar) bar.style.width = `${status.progress}%`;
const stageButtons = [...flow.querySelectorAll('[data-ux-generator-stage]')];
stageButtons.forEach(button => {
const stage = button.dataset.uxGeneratorStage;
const complete = stage === 'photos' ? state.hasPhotos : stage === 'settings' ? state.hasPhotos && state.hasPlatform : state.hasResult && state.canSave;
const current = stage === status.stage;
button.classList.toggle('is-complete', complete);
button.classList.toggle('is-current', current);
button.setAttribute('aria-current', current ? 'step' : 'false');
const label = button.querySelector('[data-ux-stage-state]');
setText(label, complete ? 'Fertig' : current ? 'Jetzt' : 'Danach');
});
const progressLabel = document.getElementById('ux-generator-progress-label');
const progressCopy = document.getElementById('ux-generator-progress-copy');
const nextTitle = document.getElementById('ux-generator-next-title');
const nextCopy = document.getElementById('ux-generator-next-copy');
const nextButton = document.getElementById('ux-generator-next-button');
if (!state.hasPhotos) {
setText(progressLabel, 'Schritt 1 von 3');
setText(progressCopy, 'Noch keine Produktfotos vorhanden.');
setText(nextTitle, 'Produktfoto hinzufügen');
setText(nextCopy, 'Ein Foto genügt zum Start; drei oder mehr Perspektiven verbessern die Erkennung.');
setText(nextButton, 'Fotos hinzufügen');
if (nextButton) nextButton.dataset.uxGeneratorAction = 'focus-photos';
} else if (!state.hasPlatform) {
setText(progressLabel, 'Schritt 2 von 3');
setText(progressCopy, `${state.photoCount} Foto${state.photoCount === 1 ? '' : 's'} bereit · Zielplattform fehlt.`);
setText(nextTitle, 'Zielplattform auswählen');
setText(nextCopy, 'Vendoo passt Titel, Beschreibung und Pflichtfelder an die gewählte Plattform an.');
setText(nextButton, 'Plattform wählen');
if (nextButton) nextButton.dataset.uxGeneratorAction = 'focus-settings';
} else if (state.loading) {
setText(progressLabel, 'AI-Analyse läuft');
setText(progressCopy, `${state.platformLabel || 'Zielplattform'} · Produktdaten werden vorbereitet.`);
setText(nextTitle, 'Analyse wird ausgeführt');
setText(nextCopy, 'Du kannst währenddessen die Fotos und Einstellungen weiterhin prüfen.');
setText(nextButton, 'Einstellungen anzeigen');
if (nextButton) nextButton.dataset.uxGeneratorAction = 'focus-settings';
} else if (!state.hasResult) {
setText(progressLabel, 'Bereit zum Generieren');
setText(progressCopy, `${state.photoCount} Foto${state.photoCount === 1 ? '' : 's'} · ${state.platformLabel || 'Plattform gewählt'}`);
setText(nextTitle, 'Artikeltext erzeugen');
setText(nextCopy, 'Die AI analysiert Produkt, Zustand und marktplatzrelevante Angaben.');
setText(nextButton, state.canGenerate ? 'Artikel generieren' : 'Einstellungen prüfen');
if (nextButton) nextButton.dataset.uxGeneratorAction = state.canGenerate ? 'generate' : 'focus-settings';
} else if (!state.canSave) {
setText(progressLabel, 'Schritt 3 von 3');
setText(progressCopy, 'Das Ergebnis ist da und benötigt noch deine Prüfung.');
setText(nextTitle, 'Vorschlag kontrollieren');
setText(nextCopy, 'Prüfe Titel, Beschreibung, Eigenschaften und Preis, bevor du speicherst.');
setText(nextButton, 'Ergebnis prüfen');
if (nextButton) nextButton.dataset.uxGeneratorAction = 'focus-result';
} else {
setText(progressLabel, 'Artikel ist speicherbereit');
setText(progressCopy, 'Alle drei Arbeitsschritte sind abgeschlossen.');
setText(nextTitle, 'Abschließend speichern');
setText(nextCopy, 'Kontrolliere die Angaben ein letztes Mal und nutze danach den Speichern-Button oben.');
setText(nextButton, 'Zum Speichern');
if (nextButton) nextButton.dataset.uxGeneratorAction = 'focus-save';
}
updateAdvancedSummary(state);
updateOptionalVisibility();
}
function scheduleUpdate() {
if (updateScheduled) return;
updateScheduled = true;
window.requestAnimationFrame(updateWorkflow);
}
function focusStage(stage) {
const selector = stageTargets[stage];
const panel = selector ? root.querySelector(selector) : null;
if (!panel) return;
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
panel.classList.add('ux-generator-panel-focus');
window.setTimeout(() => panel.classList.remove('ux-generator-panel-focus'), 900);
const focusTarget = stage === 'photos'
? document.getElementById('dropzone')
: stage === 'settings'
? document.getElementById('platform-buttons')?.querySelector('button') || document.getElementById('template-select')
: document.getElementById('result-title') || panel;
if (focusTarget) {
if (!focusTarget.hasAttribute('tabindex') && focusTarget === document.getElementById('dropzone')) focusTarget.setAttribute('tabindex', '0');
window.setTimeout(() => focusTarget.focus({ preventScroll: true }), 260);
}
}
function handleWorkflowAction(action) {
if (action === 'generate') {
const generate = document.getElementById('generate-btn');
if (generate && !generate.disabled) generate.click();
else focusStage('settings');
return;
}
if (action === 'focus-save') {
const save = document.getElementById('save-btn');
save?.scrollIntoView({ behavior: 'smooth', block: 'center' });
window.setTimeout(() => save?.focus({ preventScroll: true }), 260);
return;
}
focusStage(action.replace('focus-', ''));
}
function bindWorkflow() {
root.addEventListener('click', event => {
const stage = event.target.closest?.('[data-ux-generator-stage]');
if (stage) {
focusStage(stage.dataset.uxGeneratorStage);
return;
}
const action = event.target.closest?.('[data-ux-generator-action]');
if (action) handleWorkflowAction(action.dataset.uxGeneratorAction);
scheduleUpdate();
});
root.addEventListener('change', scheduleUpdate);
root.addEventListener('input', scheduleUpdate);
document.getElementById('reset-generator-btn')?.addEventListener('click', () => window.setTimeout(scheduleUpdate, 0));
}
function observeWorkflowInputs() {
const targets = [
document.getElementById('photo-preview'),
document.getElementById('generator-photo-count'),
document.getElementById('generator-cover'),
document.getElementById('platform-buttons'),
document.getElementById('loading'),
document.getElementById('result-section'),
document.getElementById('generate-btn'),
document.getElementById('save-btn'),
document.getElementById('generator-ai-model-card'),
].filter(Boolean);
const observer = new MutationObserver(scheduleUpdate);
targets.forEach(target => observer.observe(target, {
attributes: true,
attributeFilter: ['class', 'disabled', 'aria-pressed', 'data-selected'],
childList: true,
subtree: true,
characterData: true,
}));
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
flow = createFlow();
createAdvancedSettings();
simplifyMediaPanel();
simplifyResultPanel();
bindWorkflow();
observeWorkflowInputs();
updateWorkflow();
})();
+373
View File
@@ -0,0 +1,373 @@
.ux-nav-history {
align-items: center;
display: flex;
gap: 4px;
margin-inline: 4px 8px;
}
.ux-nav-history-button {
align-items: center;
background: var(--vd-surface, var(--surface, #fff));
block-size: 34px;
border: 1px solid var(--vd-border-subtle, var(--border-color, #d9d3cc));
border-radius: 10px;
color: var(--vd-text, var(--text-color, #26221f));
cursor: pointer;
display: inline-flex;
font: 600 18px/1 system-ui;
inline-size: 34px;
justify-content: center;
transition: transform .16s ease, border-color .16s ease, background .16s ease;
}
.ux-nav-history-button:hover:not(:disabled) {
background: var(--vd-surface-raised, var(--surface-hover, #faf7f4));
border-color: var(--vd-accent, var(--primary, #d95c34));
transform: translateY(-1px);
}
.ux-nav-history-button:disabled {
cursor: not-allowed;
opacity: .38;
}
.ux-global-search {
backdrop-filter: blur(8px);
background: color-mix(in srgb, #15110f 55%, transparent);
display: grid;
inset: 0;
padding: clamp(70px, 12vh, 140px) 18px 18px;
place-items: start center;
position: fixed;
z-index: 5000;
}
.ux-global-search.hidden {
display: none;
}
.ux-global-search-panel {
background: var(--vd-surface, var(--surface, #fff));
border: 1px solid var(--vd-border-subtle, var(--border-color, #ded7d1));
border-radius: 18px;
box-shadow: 0 28px 90px rgba(30, 20, 15, .25);
display: flex;
flex-direction: column;
inline-size: min(760px, 100%);
max-block-size: min(720px, calc(100vh - 100px));
overflow: hidden;
}
.ux-global-search-header {
align-items: flex-start;
display: flex;
justify-content: space-between;
padding: 18px 20px 10px;
}
.ux-global-search-heading {
display: grid;
gap: 3px;
}
.ux-global-search-header h2,
.ux-global-search-header p {
margin: 0;
}
.ux-global-search-header h2 {
color: var(--vd-text, var(--text-color, #26221f));
font-size: 1rem;
}
.ux-global-search-header p {
color: var(--vd-text-muted, var(--muted, #716a64));
font-size: .8rem;
}
.ux-global-search-close {
background: transparent;
block-size: 34px;
border: 0;
border-radius: 9px;
color: inherit;
cursor: pointer;
font-size: 24px;
inline-size: 34px;
}
.ux-global-search-close:hover {
background: var(--vd-surface-raised, var(--surface-hover, #faf8f6));
}
.ux-global-search-input-wrap {
align-items: center;
background: var(--vd-surface-raised, var(--surface-hover, #faf8f6));
border: 1px solid var(--vd-border-strong, var(--border-color, #cfc7c0));
border-radius: 12px;
display: flex;
gap: 12px;
margin: 0 16px 8px;
padding: 0 14px;
}
.ux-global-search-input-wrap:focus-within {
border-color: var(--vd-accent, var(--primary, #d95c34));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vd-accent, var(--primary, #d95c34)) 16%, transparent);
}
.ux-global-search-input {
background: transparent;
border: 0;
color: inherit;
flex: 1;
font: inherit;
font-size: 1rem;
min-inline-size: 0;
outline: 0;
padding: 14px 0;
}
.ux-global-search-input-wrap kbd,
.ux-global-search-footer,
.ux-global-search-status {
color: var(--vd-text-muted, var(--muted, #716a64));
font-size: .78rem;
}
.ux-global-search-input-wrap kbd,
.ux-global-search-result-meta kbd {
background: var(--vd-surface, var(--surface, #fff));
border: 1px solid var(--vd-border-subtle, var(--border-color, #ded7d1));
border-radius: 6px;
box-shadow: 0 1px 0 color-mix(in srgb, var(--vd-border-strong, #cfc7c0) 60%, transparent);
font-family: inherit;
padding: 3px 6px;
}
.ux-global-search-status {
min-block-size: 22px;
padding: 0 20px 4px;
}
.ux-global-search-results {
overflow: auto;
overscroll-behavior: contain;
padding: 2px 10px 12px;
scroll-padding-block: 34px 10px;
}
.ux-global-search-section {
color: var(--vd-text-muted, var(--muted, #716a64));
font-size: .7rem;
font-weight: 750;
letter-spacing: .06em;
padding: 12px 12px 5px;
text-transform: uppercase;
}
.ux-global-search-result {
align-items: center;
background: transparent;
border: 0;
border-radius: 11px;
color: inherit;
cursor: pointer;
display: grid;
gap: 12px;
grid-template-columns: 36px minmax(0, 1fr) auto;
inline-size: 100%;
padding: 11px 12px;
text-align: left;
}
.ux-global-search-result:hover,
.ux-global-search-result[aria-selected="true"] {
background: var(--vd-accent-soft, var(--primary-soft, #f7e4dc));
}
.ux-global-search-result.is-disabled,
.ux-global-search-result[aria-disabled="true"] {
cursor: not-allowed;
opacity: .62;
}
.ux-global-search-result.is-disabled[aria-selected="true"] {
background: var(--vd-surface-raised, var(--surface-hover, #faf8f6));
box-shadow: inset 3px 0 0 var(--vd-warning, #b66a00);
}
.ux-global-search-result-icon {
align-items: center;
background: var(--vd-surface-raised, var(--surface-hover, #faf8f6));
block-size: 34px;
border: 1px solid var(--vd-border-subtle, var(--border-color, #ded7d1));
border-radius: 10px;
display: inline-flex;
font-size: 1rem;
inline-size: 34px;
justify-content: center;
}
.ux-global-search-result-copy {
display: grid;
gap: 4px;
min-inline-size: 0;
}
.ux-global-search-result-title {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 7px;
}
.ux-global-search-result-copy strong {
font-size: .93rem;
}
.ux-global-search-result-copy small,
.ux-global-search-result-action,
.ux-global-search-result-group {
color: var(--vd-text-muted, var(--muted, #716a64));
font-size: .78rem;
}
.ux-global-search-result-copy small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ux-global-search-badge {
background: color-mix(in srgb, var(--vd-accent, var(--primary, #d95c34)) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--vd-accent, var(--primary, #d95c34)) 24%, transparent);
border-radius: 999px;
color: var(--vd-accent, var(--primary, #d95c34));
font-size: .66rem;
font-weight: 700;
padding: 2px 6px;
}
.ux-global-search-result-meta {
align-items: flex-end;
display: grid;
gap: 5px;
justify-items: end;
min-inline-size: 92px;
}
.ux-global-search-empty {
display: grid;
gap: 6px;
padding: 30px 18px;
text-align: center;
}
.ux-global-search-empty span {
color: var(--vd-text-muted, var(--muted, #716a64));
font-size: .86rem;
}
.ux-global-search-footer {
border-top: 1px solid var(--vd-border-subtle, var(--border-color, #ded7d1));
padding: 12px 18px;
text-align: center;
}
.ux-search-open {
overflow: hidden;
}
.ux-nav-history-button:focus-visible,
.ux-global-search-close:focus-visible,
.ux-global-search-result:focus-visible,
.ux-global-search-input:focus-visible {
outline: 3px solid color-mix(in srgb, var(--vd-accent, var(--primary, #d95c34)) 36%, transparent);
outline-offset: 2px;
}
@media(max-width:900px) {
.ux-nav-history {
order: 2;
}
.topbar-context-icon {
display: none;
}
.ux-nav-history-button {
block-size: 32px;
inline-size: 32px;
}
.ux-global-search {
padding-top: 72px;
}
}
@media(max-width:620px) {
.ux-nav-history {
margin-inline: 0 4px;
}
.ux-nav-history-button {
block-size: 30px;
border-radius: 8px;
inline-size: 30px;
}
.ux-global-search {
padding: 10px;
place-items: end center;
}
.ux-global-search-panel {
border-radius: 16px;
max-block-size: 88vh;
}
.ux-global-search-header {
padding: 15px 16px 8px;
}
.ux-global-search-header p,
.ux-global-search-result-group,
.ux-global-search-result-action {
display: none;
}
.ux-global-search-result {
gap: 9px;
grid-template-columns: 32px minmax(0, 1fr) auto;
padding-inline: 9px;
}
.ux-global-search-result-icon {
block-size: 30px;
border-radius: 8px;
inline-size: 30px;
}
.ux-global-search-result-meta {
min-inline-size: auto;
}
.ux-global-search-footer {
font-size: .72rem;
}
}
@media(prefers-reduced-motion:reduce) {
.ux-nav-history-button {
transition: none;
}
.ux-global-search {
backdrop-filter: none;
}
.ux-global-search-result {
scroll-behavior: auto;
}
}
+456
View File
@@ -0,0 +1,456 @@
(() => {
'use strict';
if (document.documentElement.dataset.uxGlobalNavigationReady === '1') return;
document.documentElement.dataset.uxGlobalNavigationReady = '1';
const state = {
activeView: null,
restoring: false,
query: '',
selectedIndex: 0,
commands: [],
previousFocus: null,
unsubscribeCommands: null,
};
const el = (tag, className, text) => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
};
function getActiveView() {
return document.querySelector('.sidebar-nav .nav-item.active[data-tab]')?.dataset.tab
|| document.querySelector('.tab-content.active')?.id
|| 'dashboard';
}
function getNavItem(view) {
return document.querySelector(`.sidebar-nav .nav-item[data-tab="${CSS.escape(view)}"]`);
}
function activateView(view, { replace = false } = {}) {
const item = getNavItem(view);
if (!item || item.classList.contains('hidden') || item.getAttribute('aria-hidden') === 'true') return false;
state.restoring = true;
item.closest('details.ux-nav-section')?.setAttribute('open', '');
item.click();
requestAnimationFrame(() => {
state.activeView = getActiveView();
const url = new URL(window.location.href);
url.hash = `view=${encodeURIComponent(state.activeView)}`;
const method = replace ? 'replaceState' : 'pushState';
history[method]({ vendooView: state.activeView }, '', url);
state.restoring = false;
updateHistoryButtons();
});
return true;
}
function ensureInitialHistoryState() {
const hashView = new URLSearchParams(window.location.hash.replace(/^#/, '')).get('view');
const initialItem = hashView ? getNavItem(hashView) : null;
const initial = initialItem && !initialItem.classList.contains('hidden') && initialItem.getAttribute('aria-hidden') !== 'true' ? hashView : getActiveView();
const url = new URL(window.location.href);
url.hash = `view=${encodeURIComponent(initial)}`;
history.replaceState({ vendooView: initial }, '', url);
state.activeView = initial;
if (initial !== getActiveView()) activateView(initial, { replace: true });
}
function updateHistoryButtons() {
const back = document.getElementById('ux-nav-back');
const forward = document.getElementById('ux-nav-forward');
if (back) back.disabled = history.length <= 1;
if (forward) forward.disabled = false;
}
function createHistoryControls() {
const context = document.querySelector('.topbar-context');
if (!context || document.getElementById('ux-nav-history')) return;
const controls = el('div', 'ux-nav-history');
controls.id = 'ux-nav-history';
controls.setAttribute('aria-label', 'Navigationsverlauf');
const back = el('button', 'ux-nav-history-button');
back.id = 'ux-nav-back';
back.type = 'button';
back.title = 'Zurück (Alt + Pfeil links)';
back.setAttribute('aria-label', 'Zurück');
back.textContent = '←';
const forward = el('button', 'ux-nav-history-button');
forward.id = 'ux-nav-forward';
forward.type = 'button';
forward.title = 'Vorwärts (Alt + Pfeil rechts)';
forward.setAttribute('aria-label', 'Vorwärts');
forward.textContent = '→';
back.addEventListener('click', () => history.back());
forward.addEventListener('click', () => history.forward());
controls.append(back, forward);
context.querySelector('.topbar-heading')?.before(controls);
updateHistoryButtons();
}
function getRegistry() {
const registry = window.VendooCommands;
return registry && typeof registry.list === 'function' && typeof registry.execute === 'function' ? registry : null;
}
function getPaletteCommands(query = '') {
const registry = getRegistry();
if (!registry) return [];
return registry.list({
query,
includeDisabled: true,
includeHidden: false,
context: { source: 'command-palette' },
});
}
function createDialog() {
if (document.getElementById('ux-global-search')) return document.getElementById('ux-global-search');
const dialog = el('div', 'ux-global-search hidden');
dialog.id = 'ux-global-search';
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('aria-modal', 'true');
dialog.setAttribute('aria-labelledby', 'ux-global-search-title');
dialog.setAttribute('aria-describedby', 'ux-global-search-help');
const panel = el('section', 'ux-global-search-panel');
const header = el('header', 'ux-global-search-header');
const heading = el('div', 'ux-global-search-heading');
const title = el('h2', '', 'Suchen und öffnen');
title.id = 'ux-global-search-title';
const subtitle = el('p', '', 'Bereiche und registrierte Befehle sicher finden.');
heading.append(title, subtitle);
const close = el('button', 'ux-global-search-close', '×');
close.type = 'button';
close.setAttribute('aria-label', 'Suche schließen');
const inputWrap = el('div', 'ux-global-search-input-wrap');
const input = el('input', 'ux-global-search-input');
input.id = 'ux-global-search-input';
input.type = 'search';
input.placeholder = 'Bereich, Befehl oder Stichwort suchen …';
input.autocomplete = 'off';
input.spellcheck = false;
input.setAttribute('role', 'combobox');
input.setAttribute('aria-autocomplete', 'list');
input.setAttribute('aria-controls', 'ux-global-search-results');
input.setAttribute('aria-expanded', 'false');
const shortcut = el('kbd', '', 'Esc');
inputWrap.append(input, shortcut);
const status = el('div', 'ux-global-search-status');
status.id = 'ux-global-search-status';
status.setAttribute('role', 'status');
status.setAttribute('aria-live', 'polite');
status.setAttribute('aria-atomic', 'true');
const results = el('div', 'ux-global-search-results');
results.id = 'ux-global-search-results';
results.setAttribute('role', 'listbox');
results.setAttribute('aria-label', 'Verfügbare Befehle');
const footer = el('footer', 'ux-global-search-footer');
const help = el('span', '', '↑↓ auswählen · Enter bestätigen · Esc schließen');
help.id = 'ux-global-search-help';
footer.append(help);
header.append(heading, close);
panel.append(header, inputWrap, status, results, footer);
dialog.append(panel);
document.body.append(dialog);
const isOpen = () => !dialog.classList.contains('hidden');
const updateSelection = ({ announce = false } = {}) => {
const options = [...results.querySelectorAll('[data-command-id]')];
if (!options.length) {
input.removeAttribute('aria-activedescendant');
return;
}
state.selectedIndex = Math.max(0, Math.min(state.selectedIndex, options.length - 1));
options.forEach((option, index) => option.setAttribute('aria-selected', index === state.selectedIndex ? 'true' : 'false'));
const selected = options[state.selectedIndex];
input.setAttribute('aria-activedescendant', selected.id);
selected.scrollIntoView({ block: 'nearest' });
if (announce) {
const command = state.commands[state.selectedIndex];
status.textContent = command?.enabled
? `${command.title} ausgewählt.`
: `${command?.title || 'Befehl'} nicht verfügbar: ${command?.disabledReason || 'Der Befehl ist deaktiviert.'}`;
}
};
const closeDialog = ({ restoreFocus = true } = {}) => {
if (!isOpen()) return;
dialog.classList.add('hidden');
dialog.setAttribute('aria-hidden', 'true');
document.body.classList.remove('ux-search-open');
state.query = '';
state.selectedIndex = 0;
state.commands = [];
input.value = '';
input.setAttribute('aria-expanded', 'false');
input.removeAttribute('aria-activedescendant');
results.replaceChildren();
status.textContent = '';
if (restoreFocus && state.previousFocus instanceof HTMLElement && state.previousFocus.isConnected) {
state.previousFocus.focus({ preventScroll: true });
}
state.previousFocus = null;
};
const executeCommand = commandId => {
const registry = getRegistry();
const command = state.commands.find(item => item.id === commandId);
if (!registry || !command) return;
if (!command.enabled) {
status.textContent = command.disabledReason || 'Dieser Befehl ist derzeit nicht verfügbar.';
return;
}
const executed = registry.execute(command.id, { source: 'command-palette' });
if (executed) closeDialog();
else status.textContent = command.disabledReason || 'Der Befehl konnte nicht ausgeführt werden.';
};
const executeSelected = () => {
const command = state.commands[state.selectedIndex];
if (command) executeCommand(command.id);
};
const appendSectionHeading = label => {
const headingNode = el('div', 'ux-global-search-section', label);
headingNode.setAttribute('role', 'presentation');
results.append(headingNode);
};
const appendCommand = (command, index) => {
const button = el('button', 'ux-global-search-result');
button.type = 'button';
button.id = `ux-global-search-option-${index}`;
button.dataset.commandId = command.id;
button.setAttribute('role', 'option');
button.setAttribute('aria-selected', index === state.selectedIndex ? 'true' : 'false');
button.setAttribute('aria-disabled', command.enabled ? 'false' : 'true');
if (!command.enabled) button.classList.add('is-disabled');
const icon = el('span', 'ux-global-search-result-icon', command.icon || '•');
icon.setAttribute('aria-hidden', 'true');
const copy = el('span', 'ux-global-search-result-copy');
const titleRow = el('span', 'ux-global-search-result-title');
titleRow.append(el('strong', '', command.title));
if (command.favorite) titleRow.append(el('span', 'ux-global-search-badge', 'Favorit'));
else if (command.recent) titleRow.append(el('span', 'ux-global-search-badge', 'Zuletzt'));
const description = el('small', '', command.enabled
? (command.description || command.group)
: (command.disabledReason || 'Der Befehl ist derzeit nicht verfügbar.'));
copy.append(titleRow, description);
const meta = el('span', 'ux-global-search-result-meta');
meta.append(el('span', 'ux-global-search-result-group', command.group));
if (command.shortcut) meta.append(el('kbd', '', command.shortcut));
else meta.append(el('span', 'ux-global-search-result-action', command.enabled ? 'Öffnen' : 'Nicht verfügbar'));
button.append(icon, copy, meta);
button.addEventListener('pointermove', () => {
if (state.selectedIndex === index) return;
state.selectedIndex = index;
updateSelection();
});
button.addEventListener('click', () => executeCommand(command.id));
results.append(button);
};
const render = () => {
const query = input.value.trim();
state.query = query;
state.commands = getPaletteCommands(query);
state.selectedIndex = Math.min(state.selectedIndex, Math.max(0, state.commands.length - 1));
results.replaceChildren();
if (!state.commands.length) {
const empty = el('div', 'ux-global-search-empty');
empty.append(
el('strong', '', 'Kein passender Befehl gefunden'),
el('span', '', 'Versuche Titel, Bereich, Beschreibung oder ein anderes Stichwort. Produktive Aktionen werden niemals automatisch ausgeführt.'),
);
results.append(empty);
input.removeAttribute('aria-activedescendant');
status.textContent = 'Keine passenden Befehle.';
return;
}
if (!query) {
let lastSection = '';
state.commands.forEach((command, index) => {
const section = command.favorite ? 'Favoriten' : command.recent ? 'Zuletzt geöffnet' : 'Alle Bereiche';
if (section !== lastSection) {
appendSectionHeading(section);
lastSection = section;
}
appendCommand(command, index);
});
} else {
state.commands.forEach(appendCommand);
}
status.textContent = `${state.commands.length} ${state.commands.length === 1 ? 'Befehl' : 'Befehle'} verfügbar.`;
updateSelection();
};
const openDialog = trigger => {
if (isOpen()) return;
state.previousFocus = trigger instanceof HTMLElement ? trigger : document.activeElement;
dialog.classList.remove('hidden');
dialog.setAttribute('aria-hidden', 'false');
document.body.classList.add('ux-search-open');
input.setAttribute('aria-expanded', 'true');
state.selectedIndex = 0;
render();
requestAnimationFrame(() => input.focus({ preventScroll: true }));
};
input.addEventListener('input', () => {
state.selectedIndex = 0;
render();
});
input.addEventListener('keydown', event => {
const count = state.commands.length;
if (event.key === 'ArrowDown' && count) {
event.preventDefault();
state.selectedIndex = (state.selectedIndex + 1) % count;
updateSelection({ announce: true });
}
if (event.key === 'ArrowUp' && count) {
event.preventDefault();
state.selectedIndex = (state.selectedIndex - 1 + count) % count;
updateSelection({ announce: true });
}
if (event.key === 'Home' && count) {
event.preventDefault();
state.selectedIndex = 0;
updateSelection({ announce: true });
}
if (event.key === 'End' && count) {
event.preventDefault();
state.selectedIndex = count - 1;
updateSelection({ announce: true });
}
if (event.key === 'Enter') {
event.preventDefault();
executeSelected();
}
if (event.key === 'Escape') {
event.preventDefault();
closeDialog();
}
});
close.addEventListener('click', () => closeDialog());
dialog.addEventListener('mousedown', event => {
if (event.target === dialog) closeDialog();
});
dialog.addEventListener('keydown', event => {
if (event.key !== 'Tab') return;
const focusable = [...panel.querySelectorAll('button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])')]
.filter(node => !node.hidden && node.getClientRects().length > 0);
if (!focusable.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});
dialog._open = openDialog;
dialog._close = closeDialog;
dialog._render = render;
return dialog;
}
function isEditableTarget(target) {
return target instanceof Element && Boolean(target.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]'));
}
function bindSearch() {
const dialog = createDialog();
const trigger = document.getElementById('topbar-search-btn');
trigger?.addEventListener('click', () => dialog._open(trigger));
const registry = getRegistry();
if (registry?.subscribe) {
state.unsubscribeCommands = registry.subscribe(() => {
if (!dialog.classList.contains('hidden')) dialog._render();
});
}
document.addEventListener('keydown', event => {
const modifier = /Mac|iPhone|iPad/.test(navigator.platform) ? event.metaKey : event.ctrlKey;
const paletteOpen = !dialog.classList.contains('hidden');
if (modifier && event.key.toLocaleLowerCase('de-DE') === 'k') {
if (isEditableTarget(event.target) && !paletteOpen) return;
event.preventDefault();
paletteOpen ? dialog._close() : dialog._open(event.target);
}
if (event.altKey && event.key === 'ArrowLeft') {
event.preventDefault();
history.back();
}
if (event.altKey && event.key === 'ArrowRight') {
event.preventDefault();
history.forward();
}
if (event.key === 'Escape' && paletteOpen) {
event.preventDefault();
dialog._close();
}
});
}
function observeNavigation() {
const nav = document.querySelector('.sidebar-nav');
if (!nav) return;
const observer = new MutationObserver(() => {
const next = getActiveView();
if (!next || next === state.activeView || state.restoring) return;
state.activeView = next;
const url = new URL(window.location.href);
url.hash = `view=${encodeURIComponent(next)}`;
history.pushState({ vendooView: next }, '', url);
updateHistoryButtons();
});
nav.querySelectorAll('.nav-item[data-tab]').forEach(item => observer.observe(item, {
attributes: true,
attributeFilter: ['class', 'aria-hidden'],
}));
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
window.addEventListener('popstate', event => {
const view = event.state?.vendooView || new URLSearchParams(window.location.hash.replace(/^#/, '')).get('view');
const item = view ? getNavItem(view) : null;
if (!item || item.classList.contains('hidden') || item.getAttribute('aria-hidden') === 'true') return;
state.restoring = true;
item.click();
requestAnimationFrame(() => {
state.activeView = getActiveView();
state.restoring = false;
updateHistoryButtons();
});
});
function initialize() {
createHistoryControls();
ensureInitialHistoryState();
bindSearch();
observeNavigation();
window.addEventListener('beforeunload', () => state.unsubscribeCommands?.(), { once: true });
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initialize, { once: true });
else initialize();
})();
+523
View File
@@ -0,0 +1,523 @@
#image-factory.ux-image-factory-ready {
--ux-factory-border: color-mix(in srgb, var(--vd-border, var(--border-color, #d9d7d2)) 82%, transparent);
--ux-factory-soft: color-mix(in srgb, var(--vd-surface, var(--card-bg, #fff)) 88%, var(--vd-accent, var(--accent, #db4b2f)) 12%);
--ux-factory-muted: var(--vd-text-muted, var(--text-muted, #6f6b65));
display: grid;
gap: 20px;
}
#image-factory .ux-image-factory-workflow {
display: grid;
gap: 18px;
padding: clamp(18px, 2.4vw, 28px);
border: 1px solid var(--ux-factory-border);
border-radius: var(--vd-radius-xl, 20px);
background:
radial-gradient(circle at 100% 0%, color-mix(in srgb, var(--vd-accent, var(--accent, #db4b2f)) 14%, transparent), transparent 42%),
var(--vd-surface, var(--card-bg, #fff));
box-shadow: var(--vd-shadow-sm, 0 8px 28px rgba(25, 22, 18, .06));
}
#image-factory .ux-image-factory-workflow-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
}
#image-factory .ux-image-factory-workflow-head > div {
max-width: 720px;
}
#image-factory .ux-image-factory-kicker {
display: block;
margin-bottom: 5px;
color: var(--vd-accent, var(--accent, #db4b2f));
font-size: 11px;
font-weight: 800;
letter-spacing: .12em;
text-transform: uppercase;
}
#image-factory .ux-image-factory-workflow h3,
#image-factory .ux-image-factory-stage h3 {
margin: 0;
color: var(--vd-text, var(--text-color, #292621));
}
#image-factory .ux-image-factory-workflow p,
#image-factory .ux-image-factory-stage p {
margin: 6px 0 0;
color: var(--ux-factory-muted);
line-height: 1.55;
}
#image-factory .ux-image-factory-next {
min-width: 210px;
min-height: 42px;
justify-content: center;
}
#image-factory .ux-image-factory-steps {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
#image-factory .ux-image-factory-step {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
grid-template-areas: "index copy" "state state";
align-items: center;
gap: 8px 11px;
min-width: 0;
padding: 13px;
border: 1px solid var(--ux-factory-border);
border-radius: var(--vd-radius-lg, 14px);
background: color-mix(in srgb, var(--vd-surface, var(--card-bg, #fff)) 94%, transparent);
color: inherit;
text-align: left;
cursor: pointer;
transition: border-color .18s ease, background .18s ease, transform .18s ease;
}
#image-factory .ux-image-factory-step:hover {
border-color: color-mix(in srgb, var(--vd-accent, var(--accent, #db4b2f)) 48%, var(--ux-factory-border));
transform: translateY(-1px);
}
#image-factory .ux-image-factory-step-index {
grid-area: index;
display: grid;
width: 30px;
height: 30px;
place-items: center;
border-radius: 50%;
background: var(--vd-surface-subtle, var(--muted-bg, #f2f0ec));
color: var(--ux-factory-muted);
font-size: 12px;
font-weight: 800;
}
#image-factory .ux-image-factory-step-copy {
grid-area: copy;
min-width: 0;
}
#image-factory .ux-image-factory-step-copy strong,
#image-factory .ux-image-factory-step-copy small {
display: block;
}
#image-factory .ux-image-factory-step-copy strong {
overflow: hidden;
color: var(--vd-text, var(--text-color, #292621));
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
#image-factory .ux-image-factory-step-copy small {
margin-top: 2px;
overflow: hidden;
color: var(--ux-factory-muted);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
#image-factory .ux-image-factory-step-state {
grid-area: state;
justify-self: start;
padding: 3px 8px;
border-radius: 999px;
background: var(--vd-surface-subtle, var(--muted-bg, #f2f0ec));
color: var(--ux-factory-muted);
font-size: 10px;
font-weight: 800;
letter-spacing: .04em;
text-transform: uppercase;
}
#image-factory .ux-image-factory-step.is-current {
border-color: color-mix(in srgb, var(--vd-accent, var(--accent, #db4b2f)) 62%, var(--ux-factory-border));
background: var(--ux-factory-soft);
}
#image-factory .ux-image-factory-step.is-current .ux-image-factory-step-index,
#image-factory .ux-image-factory-step.is-current .ux-image-factory-step-state {
background: var(--vd-accent, var(--accent, #db4b2f));
color: var(--vd-on-accent, #fff);
}
#image-factory .ux-image-factory-step.is-complete .ux-image-factory-step-index {
background: var(--vd-success, #2d8a61);
color: #fff;
}
#image-factory .ux-image-factory-step.is-complete .ux-image-factory-step-state {
background: color-mix(in srgb, var(--vd-success, #2d8a61) 15%, transparent);
color: var(--vd-success, #2d8a61);
}
#image-factory .image-factory-layout {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 16px;
}
#image-factory .image-factory-config {
display: grid;
gap: 16px;
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
}
#image-factory .ux-image-factory-stage {
scroll-margin-top: 92px;
padding: clamp(17px, 2.2vw, 24px);
border: 1px solid var(--ux-factory-border);
border-radius: var(--vd-radius-xl, 18px);
background: var(--vd-surface, var(--card-bg, #fff));
box-shadow: var(--vd-shadow-xs, 0 4px 18px rgba(25, 22, 18, .045));
}
#image-factory .ux-image-factory-stage-head {
display: flex;
align-items: flex-start;
gap: 13px;
margin-bottom: 18px;
}
#image-factory .ux-image-factory-stage-index {
flex: 0 0 auto;
display: grid;
width: 38px;
height: 38px;
place-items: center;
border-radius: 12px;
background: var(--vd-surface-subtle, var(--muted-bg, #f2f0ec));
color: var(--vd-accent, var(--accent, #db4b2f));
font-size: 12px;
font-weight: 900;
letter-spacing: .05em;
}
#image-factory[data-ux-factory-current-step="sources"] .ux-image-factory-stage-sources,
#image-factory[data-ux-factory-current-step="profiles"] .ux-image-factory-stage-profiles,
#image-factory[data-ux-factory-current-step="review"] .ux-image-factory-stage-review {
border-color: color-mix(in srgb, var(--vd-accent, var(--accent, #db4b2f)) 48%, var(--ux-factory-border));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vd-accent, var(--accent, #db4b2f)) 8%, transparent);
}
#image-factory .image-factory-source-head,
#image-factory .image-factory-profile-head {
margin: 0 0 12px;
}
#image-factory .image-factory-source-head .image-factory-source-tools:empty {
display: none;
}
#image-factory #image-factory-source-search {
width: min(520px, 100%);
min-height: 42px;
margin-bottom: 10px;
}
#image-factory .ux-image-factory-source-controls {
margin-bottom: 14px;
border: 1px solid var(--ux-factory-border);
border-radius: var(--vd-radius-md, 12px);
background: var(--vd-surface-subtle, var(--muted-bg, #f7f5f2));
}
#image-factory .ux-image-factory-source-controls > summary,
#image-factory .ux-image-factory-monitor > summary {
list-style: none;
cursor: pointer;
}
#image-factory .ux-image-factory-source-controls > summary::-webkit-details-marker,
#image-factory .ux-image-factory-monitor > summary::-webkit-details-marker {
display: none;
}
#image-factory .ux-image-factory-source-controls > summary {
padding: 10px 13px;
color: var(--vd-text, var(--text-color, #292621));
font-size: 12px;
font-weight: 750;
}
#image-factory .ux-image-factory-source-controls > summary::after {
content: "";
float: right;
color: var(--ux-factory-muted);
}
#image-factory .ux-image-factory-source-controls[open] > summary::after {
content: "";
}
#image-factory .ux-image-factory-source-controls-body {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 0 12px 12px;
}
#image-factory .ux-image-factory-source-controls-body > * {
min-height: 36px;
}
#image-factory .image-factory-source-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(142px, 1fr));
gap: 10px;
max-height: min(55vh, 560px);
overflow: auto;
padding: 3px;
scrollbar-gutter: stable;
}
#image-factory .image-factory-source-grid > * {
min-width: 0;
border-radius: var(--vd-radius-md, 12px);
}
#image-factory .image-factory-profile-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: 11px;
}
#image-factory .image-factory-profile-grid > * {
min-width: 0;
border-radius: var(--vd-radius-md, 12px);
}
#image-factory .ux-image-factory-stage-review > label.field {
max-width: 720px;
}
#image-factory .ux-image-factory-advanced {
margin: 16px 0;
border: 1px solid var(--ux-factory-border);
border-radius: var(--vd-radius-lg, 14px);
background: var(--vd-surface-subtle, var(--muted-bg, #f7f5f2));
}
#image-factory .ux-image-factory-advanced > summary {
padding: 14px 16px;
font-weight: 760;
}
#image-factory .ux-image-factory-advanced .image-factory-options-grid {
padding: 0 16px 16px;
}
#image-factory .image-factory-submit {
position: sticky;
bottom: 12px;
z-index: 4;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
margin-top: 18px;
padding: 14px 16px;
border: 1px solid color-mix(in srgb, var(--vd-accent, var(--accent, #db4b2f)) 25%, var(--ux-factory-border));
border-radius: var(--vd-radius-lg, 14px);
background: color-mix(in srgb, var(--vd-surface, var(--card-bg, #fff)) 94%, transparent);
box-shadow: 0 12px 28px rgba(25, 22, 18, .12);
backdrop-filter: blur(14px);
}
#image-factory[data-ux-factory-ready="false"] .image-factory-submit {
opacity: .72;
}
#image-factory .ux-image-factory-monitor {
overflow: hidden;
border: 1px solid var(--ux-factory-border);
border-radius: var(--vd-radius-xl, 18px);
background: var(--vd-surface, var(--card-bg, #fff));
}
#image-factory .ux-image-factory-monitor > summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 15px 18px;
}
#image-factory .ux-image-factory-monitor > summary > span:first-child strong,
#image-factory .ux-image-factory-monitor > summary > span:first-child small {
display: block;
}
#image-factory .ux-image-factory-monitor > summary > span:first-child small {
margin-top: 3px;
color: var(--ux-factory-muted);
}
#image-factory .ux-image-factory-monitor-badge {
flex: 0 0 auto;
padding: 5px 10px;
border-radius: 999px;
background: var(--vd-surface-subtle, var(--muted-bg, #f2f0ec));
color: var(--ux-factory-muted);
font-size: 11px;
font-weight: 800;
}
#image-factory .ux-image-factory-monitor-badge[data-tone="working"] {
background: color-mix(in srgb, var(--vd-warning, #b77712) 14%, transparent);
color: var(--vd-warning, #9a650e);
}
#image-factory .ux-image-factory-monitor-badge[data-tone="danger"] {
background: color-mix(in srgb, var(--vd-danger, #c63f3f) 14%, transparent);
color: var(--vd-danger, #b12f2f);
}
#image-factory .ux-image-factory-monitor-badge[data-tone="success"] {
background: color-mix(in srgb, var(--vd-success, #2d8a61) 14%, transparent);
color: var(--vd-success, #2d8a61);
}
#image-factory .ux-image-factory-monitor .image-factory-status {
width: auto;
margin: 0;
padding: 0 18px 18px;
border: 0;
background: transparent;
box-shadow: none;
}
#image-factory .image-factory-jobs {
scroll-margin-top: 92px;
padding: clamp(17px, 2.2vw, 24px);
border-radius: var(--vd-radius-xl, 18px);
}
#image-factory .ux-image-factory-job-filter {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin: 14px 0 16px;
}
#image-factory .ux-image-factory-job-filter button.active {
border-color: var(--vd-accent, var(--accent, #db4b2f));
background: var(--ux-factory-soft);
color: var(--vd-accent, var(--accent, #db4b2f));
}
#image-factory #image-factory-jobs {
display: grid;
gap: 10px;
}
#image-factory #image-factory-jobs > * {
border-radius: var(--vd-radius-md, 12px);
}
#image-factory .ux-image-factory-step:focus-visible,
#image-factory .ux-image-factory-next:focus-visible,
#image-factory .ux-image-factory-job-filter button:focus-visible,
#image-factory .ux-image-factory-source-controls > summary:focus-visible,
#image-factory .ux-image-factory-monitor > summary:focus-visible {
outline: 3px solid color-mix(in srgb, var(--vd-accent, var(--accent, #db4b2f)) 34%, transparent);
outline-offset: 3px;
}
@media (max-width: 1080px) {
#image-factory .ux-image-factory-steps {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#image-factory .image-factory-source-grid {
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
}
}
@media (max-width: 760px) {
#image-factory .ux-image-factory-workflow-head,
#image-factory .image-factory-submit,
#image-factory .ux-image-factory-monitor > summary,
#image-factory .image-factory-jobs > .section-title-row {
align-items: stretch;
flex-direction: column;
}
#image-factory .ux-image-factory-next {
width: 100%;
}
#image-factory .ux-image-factory-steps {
grid-template-columns: minmax(0, 1fr);
}
#image-factory .ux-image-factory-step {
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-areas: "index copy state";
}
#image-factory .ux-image-factory-step-state {
grid-area: state;
align-self: center;
}
#image-factory .image-factory-profile-grid {
grid-template-columns: minmax(0, 1fr);
}
#image-factory .image-factory-submit {
position: static;
box-shadow: none;
}
#image-factory .image-factory-submit button {
width: 100%;
}
}
@media (max-width: 470px) {
#image-factory .ux-image-factory-workflow,
#image-factory .ux-image-factory-stage,
#image-factory .image-factory-jobs {
padding: 14px;
border-radius: var(--vd-radius-lg, 14px);
}
#image-factory .image-factory-source-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
max-height: 52vh;
}
#image-factory .ux-image-factory-source-controls-body,
#image-factory .ux-image-factory-job-filter {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#image-factory .ux-image-factory-source-controls-body > *,
#image-factory .ux-image-factory-job-filter button {
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
#image-factory .ux-image-factory-step {
transition: none;
}
#image-factory .ux-image-factory-step:hover {
transform: none;
}
}
+366
View File
@@ -0,0 +1,366 @@
(() => {
'use strict';
const ROOT_ID = 'image-factory';
const JOB_FILTER_KEY = 'vendoo_image_factory_job_filter';
const JOB_FILTERS = ['all', 'active', 'completed', 'issues'];
const createNode = (tag, className = '', text = '') => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text) node.textContent = text;
return node;
};
const scrollToNode = node => {
if (!node) return;
node.scrollIntoView({ behavior: 'smooth', block: 'start' });
window.requestAnimationFrame(() => {
const focusTarget = node.matches('input,button,select,textarea') ? node : node.querySelector('input,button,select,textarea');
focusTarget?.focus({ preventScroll: true });
});
};
function parseSelectionSummary(root) {
const summary = root.querySelector('#image-factory-selection-count')?.textContent || '';
const match = summary.match(/(\d+)\s+Bilder?.*?(\d+)\s+Profile?/i);
if (match) return { sources: Number(match[1]), profiles: Number(match[2]) };
const sourceGrid = root.querySelector('#image-factory-source-grid');
const profileGrid = root.querySelector('#image-factory-profile-grid');
const countSelected = container => {
if (!container) return 0;
const checked = container.querySelectorAll('input[type="checkbox"]:checked, input[type="radio"]:checked').length;
if (checked) return checked;
return container.querySelectorAll('.is-selected, .selected, [aria-pressed="true"]').length;
};
return { sources: countSelected(sourceGrid), profiles: countSelected(profileGrid) };
}
function productionCounts(root) {
const read = status => Number(root.querySelector(`[data-batch-summary="${status}"]`)?.textContent || 0);
return {
queued: read('queued'),
running: read('running'),
paused: read('paused'),
completed: read('completed'),
partial: read('partial'),
failed: read('failed'),
};
}
function createStep(number, id, title, copy) {
const button = createNode('button', 'ux-image-factory-step');
button.type = 'button';
button.dataset.uxFactoryStep = id;
button.setAttribute('aria-pressed', 'false');
const index = createNode('span', 'ux-image-factory-step-index', String(number));
const body = createNode('span', 'ux-image-factory-step-copy');
body.append(createNode('strong', '', title), createNode('small', '', copy));
const state = createNode('span', 'ux-image-factory-step-state', 'Offen');
state.dataset.uxFactoryStepState = id;
button.append(index, body, state);
return button;
}
function createWorkflow(root) {
if (root.querySelector('.ux-image-factory-workflow')) return;
const layout = root.querySelector('.image-factory-layout');
if (!layout) return;
const workflow = createNode('section', 'ux-image-factory-workflow');
workflow.setAttribute('aria-label', 'Ablauf der Bildproduktion');
const heading = createNode('div', 'ux-image-factory-workflow-head');
const headingCopy = createNode('div');
headingCopy.append(
createNode('span', 'ux-image-factory-kicker', 'Geführte Bildproduktion'),
createNode('h3', '', 'Vom Quellbild zur fertigen Ausgabe'),
createNode('p', '', 'Vendoo zeigt nur den jeweils relevanten Arbeitsschritt. Bestehende Profile, Jobs und Dateien bleiben unverändert.')
);
const next = createNode('button', 'vd-button primary ux-image-factory-next', 'Quellbilder auswählen');
next.type = 'button';
next.dataset.uxFactoryNext = 'sources';
heading.append(headingCopy, next);
const steps = createNode('div', 'ux-image-factory-steps');
steps.append(
createStep(1, 'sources', 'Quellbilder', 'Bilder für den Auftrag wählen'),
createStep(2, 'profiles', 'Ausgabeprofile', 'Formate und Qualitätsziele festlegen'),
createStep(3, 'review', 'Prüfen & starten', 'Name, Optionen und Umfang kontrollieren'),
createStep(4, 'jobs', 'Aufträge & Ergebnisse', 'Fortschritt und Fehler überwachen')
);
workflow.append(heading, steps);
layout.before(workflow);
}
function createStage(id, number, title, copy) {
const stage = createNode('section', `ux-image-factory-stage ux-image-factory-stage-${id}`);
stage.dataset.uxFactoryStage = id;
stage.id = `ux-image-factory-stage-${id}`;
const head = createNode('header', 'ux-image-factory-stage-head');
const index = createNode('span', 'ux-image-factory-stage-index', String(number).padStart(2, '0'));
const text = createNode('div');
text.append(createNode('h3', '', title), createNode('p', '', copy));
head.append(index, text);
stage.append(head);
return stage;
}
function restructureConfiguration(root) {
const config = root.querySelector('.image-factory-config');
if (!config || config.dataset.uxFactoryStructured === 'true') return;
const sourceHead = config.querySelector('.image-factory-source-head');
const sourceGrid = config.querySelector('#image-factory-source-grid');
const profileHead = config.querySelector('.image-factory-profile-head');
const profileGrid = config.querySelector('#image-factory-profile-grid');
const nameField = config.querySelector(':scope > label.field');
const options = config.querySelector('.image-factory-options');
const submit = config.querySelector('.image-factory-submit');
if (!sourceHead || !sourceGrid || !profileHead || !profileGrid || !submit) return;
const sourceStage = createStage('sources', 1, 'Quellbilder auswählen', 'Suche oder filtere die Medienbibliothek und markiere die Bilder, die verarbeitet werden sollen.');
const sourceTools = sourceHead.querySelector('.image-factory-source-tools');
const sourceSearch = sourceTools?.querySelector('#image-factory-source-search');
const sourceControls = createNode('details', 'ux-image-factory-source-controls');
const sourceControlsSummary = createNode('summary', '', 'Auswahlwerkzeuge');
const sourceControlsBody = createNode('div', 'ux-image-factory-source-controls-body');
if (sourceTools) {
if (sourceSearch) sourceStage.append(sourceSearch);
[...sourceTools.children].forEach(child => sourceControlsBody.append(child));
sourceControls.append(sourceControlsSummary, sourceControlsBody);
}
sourceStage.append(sourceHead, sourceControls, sourceGrid);
const profileStage = createStage('profiles', 2, 'Ausgabeprofile festlegen', 'Wähle ein oder mehrere Zielformate. Jedes Profil erzeugt pro Quellbild eine eigenständige Ausgabe.');
profileStage.append(profileHead, profileGrid);
const reviewStage = createStage('review', 3, 'Auftrag prüfen und starten', 'Vergib einen Namen, kontrolliere den Ausgabeumfang und aktiviere nur bei Bedarf zusätzliche Verarbeitung.');
if (nameField) reviewStage.append(nameField);
if (options) {
options.open = false;
options.classList.add('ux-image-factory-advanced');
reviewStage.append(options);
}
reviewStage.append(submit);
const originalTitle = config.querySelector(':scope > .section-title-row');
originalTitle?.remove();
config.append(sourceStage, profileStage, reviewStage);
config.dataset.uxFactoryStructured = 'true';
}
function createMonitor(root) {
const status = root.querySelector('.image-factory-status');
const layout = root.querySelector('.image-factory-layout');
if (!status || !layout || root.querySelector('.ux-image-factory-monitor')) return;
const monitor = createNode('details', 'ux-image-factory-monitor');
const summary = createNode('summary');
const summaryCopy = createNode('span');
summaryCopy.append(createNode('strong', '', 'Produktionsmonitor'), createNode('small', '', 'Wartende, laufende und fehlerhafte Aufträge'));
const badge = createNode('span', 'ux-image-factory-monitor-badge', 'Keine aktiven Aufträge');
badge.dataset.uxFactoryMonitorBadge = 'true';
summary.append(summaryCopy, badge);
monitor.append(summary, status);
layout.append(monitor);
}
function createJobControls(root) {
const jobs = root.querySelector('.image-factory-jobs');
const list = root.querySelector('#image-factory-jobs');
if (!jobs || !list || jobs.querySelector('.ux-image-factory-job-filter')) return;
jobs.id = 'ux-image-factory-stage-jobs';
const filter = createNode('div', 'ux-image-factory-job-filter');
filter.setAttribute('role', 'group');
filter.setAttribute('aria-label', 'Produktionsaufträge filtern');
const definitions = [
['all', 'Alle'],
['active', 'Aktiv'],
['completed', 'Fertig'],
['issues', 'Probleme'],
];
definitions.forEach(([value, label]) => {
const button = createNode('button', 'vd-button compact', label);
button.type = 'button';
button.dataset.uxFactoryJobFilter = value;
button.setAttribute('aria-pressed', 'false');
filter.append(button);
});
const titleRow = jobs.querySelector(':scope > .section-title-row');
titleRow?.after(filter);
let stored = 'all';
try { stored = localStorage.getItem(JOB_FILTER_KEY) || 'all'; } catch {}
root.dataset.uxFactoryJobFilter = JOB_FILTERS.includes(stored) ? stored : 'all';
}
function classifyJob(node) {
if (node.classList.contains('empty-state')) return 'empty';
const text = (node.textContent || '').toLowerCase();
if (/fehler|fehlgeschlagen|teilweise|failed|partial|abgebrochen|cancelled/.test(text)) return 'issues';
if (/wartend|läuft|laufend|pausiert|queued|running|paused|in bearbeitung/.test(text)) return 'active';
if (/fertig|abgeschlossen|completed|erfolgreich|success/.test(text)) return 'completed';
return 'other';
}
function applyJobFilter(root) {
const selected = JOB_FILTERS.includes(root.dataset.uxFactoryJobFilter) ? root.dataset.uxFactoryJobFilter : 'all';
root.querySelectorAll('[data-ux-factory-job-filter]').forEach(button => {
const active = button.dataset.uxFactoryJobFilter === selected;
button.classList.toggle('active', active);
button.setAttribute('aria-pressed', String(active));
});
const list = root.querySelector('#image-factory-jobs');
if (!list) return;
[...list.children].forEach(job => {
const category = classifyJob(job);
job.hidden = selected !== 'all' && category !== 'empty' && category !== selected;
});
}
function updateWorkflow(root) {
const selection = parseSelectionSummary(root);
const counts = productionCounts(root);
const hasSources = selection.sources > 0;
const hasProfiles = selection.profiles > 0;
const ready = hasSources && hasProfiles;
const activeJobs = counts.queued + counts.running + counts.paused;
const issues = counts.failed + counts.partial;
const hasJobs = activeJobs + counts.completed + issues > 0;
root.dataset.uxFactoryHasSources = String(hasSources);
root.dataset.uxFactoryHasProfiles = String(hasProfiles);
root.dataset.uxFactoryReady = String(ready);
root.dataset.uxFactoryHasJobs = String(hasJobs);
const current = !hasSources ? 'sources' : !hasProfiles ? 'profiles' : activeJobs || issues ? 'jobs' : 'review';
root.dataset.uxFactoryCurrentStep = current;
const completed = {
sources: hasSources,
profiles: hasProfiles,
review: ready && hasJobs,
jobs: hasJobs && activeJobs === 0 && issues === 0,
};
root.querySelectorAll('[data-ux-factory-step]').forEach(step => {
const id = step.dataset.uxFactoryStep;
const isCurrent = id === current;
step.classList.toggle('is-current', isCurrent);
step.classList.toggle('is-complete', Boolean(completed[id]));
step.setAttribute('aria-pressed', String(isCurrent));
const state = step.querySelector('[data-ux-factory-step-state]');
if (state) state.textContent = completed[id] ? 'Erledigt' : isCurrent ? 'Jetzt' : 'Offen';
});
const next = root.querySelector('[data-ux-factory-next]');
if (next) {
if (!hasSources) {
next.dataset.uxFactoryNext = 'sources';
next.textContent = 'Quellbilder auswählen';
} else if (!hasProfiles) {
next.dataset.uxFactoryNext = 'profiles';
next.textContent = 'Ausgabeprofile wählen';
} else if (activeJobs || issues) {
next.dataset.uxFactoryNext = 'jobs';
next.textContent = issues ? 'Probleme prüfen' : 'Produktion überwachen';
} else {
next.dataset.uxFactoryNext = 'submit';
next.textContent = 'Auftrag prüfen & starten';
}
}
const badge = root.querySelector('[data-ux-factory-monitor-badge]');
if (badge) {
badge.textContent = issues
? `${issues} Problem${issues === 1 ? '' : 'e'}`
: activeJobs
? `${activeJobs} aktiv`
: counts.completed
? `${counts.completed} fertig`
: 'Keine aktiven Aufträge';
badge.dataset.tone = issues ? 'danger' : activeJobs ? 'working' : counts.completed ? 'success' : 'neutral';
}
const monitor = root.querySelector('.ux-image-factory-monitor');
if (monitor && !monitor.dataset.uxFactoryInitialized) {
monitor.open = activeJobs > 0 || issues > 0;
monitor.dataset.uxFactoryInitialized = 'true';
}
applyJobFilter(root);
}
function stageTarget(root, id) {
if (id === 'jobs') return root.querySelector('#ux-image-factory-stage-jobs');
return root.querySelector(`#ux-image-factory-stage-${id}`);
}
function bindActions(root) {
root.addEventListener('click', event => {
const step = event.target.closest('[data-ux-factory-step]');
if (step) {
scrollToNode(stageTarget(root, step.dataset.uxFactoryStep));
return;
}
const next = event.target.closest('[data-ux-factory-next]');
if (next) {
const target = next.dataset.uxFactoryNext;
if (target === 'submit') {
scrollToNode(stageTarget(root, 'review'));
root.querySelector('#image-factory-submit')?.focus({ preventScroll: true });
} else {
scrollToNode(stageTarget(root, target));
}
return;
}
const filter = event.target.closest('[data-ux-factory-job-filter]');
if (filter) {
root.dataset.uxFactoryJobFilter = filter.dataset.uxFactoryJobFilter;
try { localStorage.setItem(JOB_FILTER_KEY, root.dataset.uxFactoryJobFilter); } catch {}
applyJobFilter(root);
}
});
}
function observeFactory(root) {
const observer = new MutationObserver(() => window.requestAnimationFrame(() => updateWorkflow(root)));
const targets = [
root.querySelector('#image-factory-selection-count'),
root.querySelector('#image-factory-source-grid'),
root.querySelector('#image-factory-profile-grid'),
root.querySelector('#image-factory-summary'),
root.querySelector('#image-factory-jobs'),
].filter(Boolean);
targets.forEach(target => observer.observe(target, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['class', 'checked', 'aria-pressed'] }));
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
function initializeImageFactoryUx() {
const root = document.getElementById(ROOT_ID);
if (!root || root.dataset.uxImageFactoryReady === 'true') return;
createWorkflow(root);
restructureConfiguration(root);
createMonitor(root);
createJobControls(root);
bindActions(root);
observeFactory(root);
root.dataset.uxImageFactoryReady = 'true';
root.classList.add('ux-image-factory-ready');
updateWorkflow(root);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeImageFactoryUx, { once: true });
} else {
initializeImageFactoryUx();
}
})();
+533
View File
@@ -0,0 +1,533 @@
#inventory.ux-inventory-ready {
--ux-inventory-gap: clamp(14px, 1.8vw, 24px);
}
#inventory.ux-inventory-ready .inventory-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: var(--ux-inventory-gap);
}
#inventory.ux-inventory-ready .inventory-header h2 {
margin: 0;
}
#inventory.ux-inventory-ready .ux-inventory-overview {
display: grid;
gap: 18px;
margin-bottom: var(--ux-inventory-gap);
padding: clamp(18px, 2.2vw, 28px);
border: 1px solid var(--vd-border, rgba(33, 30, 27, .12));
border-radius: var(--vd-radius-xl, 22px);
background: linear-gradient(135deg, color-mix(in srgb, var(--vd-accent, #d95f35) 9%, var(--vd-surface, #fff)), var(--vd-surface, #fff));
box-shadow: var(--vd-shadow-sm, 0 10px 30px rgba(26, 23, 20, .06));
}
#inventory.ux-inventory-ready .ux-inventory-overview-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
}
#inventory.ux-inventory-ready .ux-inventory-kicker {
display: block;
margin-bottom: 5px;
color: var(--vd-accent, #d95f35);
font-size: .76rem;
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
}
#inventory.ux-inventory-ready .ux-inventory-overview h3,
#inventory.ux-inventory-ready .ux-inventory-overview p {
margin: 0;
}
#inventory.ux-inventory-ready .ux-inventory-overview h3 {
font-size: clamp(1.15rem, 2vw, 1.55rem);
}
#inventory.ux-inventory-ready .ux-inventory-overview p {
max-width: 760px;
margin-top: 6px;
color: var(--vd-text-muted, #6f675f);
line-height: 1.55;
}
#inventory.ux-inventory-ready .ux-inventory-next {
flex: 0 0 auto;
min-height: 42px;
}
#inventory.ux-inventory-ready .ux-inventory-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
#inventory.ux-inventory-ready .ux-inventory-metric {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
padding: 14px;
border: 1px solid color-mix(in srgb, var(--vd-border, #ddd) 76%, transparent);
border-radius: var(--vd-radius-lg, 16px);
background: color-mix(in srgb, var(--vd-surface, #fff) 88%, transparent);
}
#inventory.ux-inventory-ready .ux-inventory-metric-value {
min-width: 48px;
color: var(--vd-text, #26211d);
font-size: clamp(1.25rem, 2.3vw, 1.75rem);
line-height: 1;
}
#inventory.ux-inventory-ready .ux-inventory-metric-copy {
display: grid;
min-width: 0;
gap: 2px;
}
#inventory.ux-inventory-ready .ux-inventory-metric-copy b {
font-size: .9rem;
}
#inventory.ux-inventory-ready .ux-inventory-metric-copy small {
overflow: hidden;
color: var(--vd-text-muted, #6f675f);
text-overflow: ellipsis;
white-space: nowrap;
}
#inventory.ux-inventory-ready.ux-inventory-has-unassigned .ux-inventory-metric:first-child {
border-color: color-mix(in srgb, var(--vd-danger, #b42318) 42%, var(--vd-border, #ddd));
background: color-mix(in srgb, var(--vd-danger, #b42318) 7%, var(--vd-surface, #fff));
}
#inventory.ux-inventory-ready.ux-inventory-has-unassigned .ux-inventory-metric:first-child .ux-inventory-metric-value {
color: var(--vd-danger, #b42318);
}
#inventory.ux-inventory-ready .ux-inventory-stats-secondary {
margin-bottom: 12px;
opacity: .82;
}
#inventory.ux-inventory-ready .ux-inventory-toolbar {
display: grid;
gap: 14px;
margin-bottom: 14px;
padding: 16px;
}
#inventory.ux-inventory-ready .ux-inventory-toolbar-primary,
#inventory.ux-inventory-ready .ux-inventory-controls {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 14px;
}
#inventory.ux-inventory-ready .ux-inventory-search,
#inventory.ux-inventory-ready .ux-inventory-sort,
#inventory.ux-inventory-ready .ux-inventory-detail-search {
display: grid;
gap: 6px;
}
#inventory.ux-inventory-ready .ux-inventory-search {
flex: 1 1 420px;
max-width: 640px;
}
#inventory.ux-inventory-ready .ux-inventory-search span,
#inventory.ux-inventory-ready .ux-inventory-sort span,
#inventory.ux-inventory-ready .ux-inventory-detail-search span {
color: var(--vd-text-muted, #6f675f);
font-size: .76rem;
font-weight: 750;
}
#inventory.ux-inventory-ready .ux-inventory-search input,
#inventory.ux-inventory-ready .ux-inventory-sort select,
#inventory.ux-inventory-ready .ux-inventory-detail-search input {
min-height: 42px;
border: 1px solid var(--vd-border, #ddd);
border-radius: var(--vd-radius-md, 12px);
background: var(--vd-surface, #fff);
color: var(--vd-text, #26211d);
}
#inventory.ux-inventory-ready .ux-inventory-search input,
#inventory.ux-inventory-ready .ux-inventory-detail-search input {
width: 100%;
padding: 0 13px;
}
#inventory.ux-inventory-ready .ux-inventory-sort select {
padding: 0 36px 0 12px;
}
#inventory.ux-inventory-ready .ux-inventory-visible-count {
flex: 0 0 auto;
padding-bottom: 11px;
color: var(--vd-text-muted, #6f675f);
font-size: .85rem;
font-weight: 700;
}
#inventory.ux-inventory-ready .ux-inventory-filter-group {
display: inline-flex;
flex-wrap: wrap;
gap: 6px;
}
#inventory.ux-inventory-ready .ux-inventory-filter {
min-height: 38px;
padding: 0 13px;
border: 1px solid var(--vd-border, #ddd);
border-radius: 999px;
background: var(--vd-surface, #fff);
color: var(--vd-text-muted, #6f675f);
font: inherit;
font-size: .84rem;
font-weight: 750;
cursor: pointer;
}
#inventory.ux-inventory-ready .ux-inventory-filter[aria-pressed="true"] {
border-color: color-mix(in srgb, var(--vd-accent, #d95f35) 52%, var(--vd-border, #ddd));
background: color-mix(in srgb, var(--vd-accent, #d95f35) 11%, var(--vd-surface, #fff));
color: var(--vd-accent-strong, #9c3d1d);
}
#inventory.ux-inventory-ready .inventory-locations {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 14px;
}
#inventory.ux-inventory-ready .inv-location-card {
position: relative;
display: grid;
gap: 13px;
min-height: 156px;
padding: 18px;
border: 1px solid var(--vd-border, rgba(33, 30, 27, .12));
border-radius: var(--vd-radius-lg, 16px);
background: var(--vd-surface, #fff);
box-shadow: var(--vd-shadow-xs, 0 4px 14px rgba(26, 23, 20, .04));
cursor: pointer;
transition: transform .16s ease, border-color .16s ease, box-shadow .16s ease;
}
#inventory.ux-inventory-ready .inv-location-card:hover {
transform: translateY(-2px);
border-color: color-mix(in srgb, var(--vd-accent, #d95f35) 36%, var(--vd-border, #ddd));
box-shadow: var(--vd-shadow-sm, 0 10px 30px rgba(26, 23, 20, .07));
}
#inventory.ux-inventory-ready .inv-location-card.ux-inventory-needs-attention {
border-color: color-mix(in srgb, var(--vd-warning, #b7791f) 48%, var(--vd-border, #ddd));
background: color-mix(in srgb, var(--vd-warning, #b7791f) 6%, var(--vd-surface, #fff));
}
#inventory.ux-inventory-ready .inv-location-card.ux-inventory-needs-attention::after {
content: "Zuweisung erforderlich";
position: absolute;
top: 13px;
right: 13px;
padding: 5px 8px;
border-radius: 999px;
background: color-mix(in srgb, var(--vd-warning, #b7791f) 15%, var(--vd-surface, #fff));
color: var(--vd-warning-strong, #7a4d08);
font-size: .7rem;
font-weight: 800;
}
#inventory.ux-inventory-ready .inv-location-card.ux-inventory-filtered-out,
#inventory.ux-inventory-ready .history-item.ux-inventory-detail-filtered {
display: none !important;
}
#inventory.ux-inventory-ready .inv-loc-header {
padding-right: 88px;
}
#inventory.ux-inventory-ready .inv-location-card:not(.ux-inventory-needs-attention) .inv-loc-header {
padding-right: 0;
}
#inventory.ux-inventory-ready .inv-loc-name {
overflow-wrap: anywhere;
font-size: 1rem;
font-weight: 800;
}
#inventory.ux-inventory-ready .inv-loc-stats {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
#inventory.ux-inventory-ready .inv-loc-stats span {
padding: 5px 8px;
border-radius: 999px;
background: var(--vd-surface-subtle, #f4f1ed);
color: var(--vd-text-muted, #6f675f);
font-size: .75rem;
font-weight: 700;
}
#inventory.ux-inventory-ready .inv-loc-value {
align-self: end;
color: var(--vd-text, #26211d);
font-size: 1.1rem;
font-weight: 850;
}
#inventory.ux-inventory-ready .inventory-detail {
display: grid;
gap: 14px;
}
#inventory.ux-inventory-ready .ux-inventory-detail-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
#inventory.ux-inventory-ready .ux-inventory-detail-title-wrap {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
#inventory.ux-inventory-ready .ux-inventory-detail-title-wrap h3 {
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#inventory.ux-inventory-ready .ux-inventory-more-actions {
position: relative;
}
#inventory.ux-inventory-ready .ux-inventory-more-actions > summary {
min-height: 38px;
padding: 9px 12px;
border: 1px solid var(--vd-border, #ddd);
border-radius: var(--vd-radius-md, 12px);
background: var(--vd-surface, #fff);
color: var(--vd-text-muted, #6f675f);
font-size: .82rem;
font-weight: 750;
cursor: pointer;
list-style: none;
}
#inventory.ux-inventory-ready .ux-inventory-more-actions > summary::-webkit-details-marker {
display: none;
}
#inventory.ux-inventory-ready .ux-inventory-more-actions-body {
position: absolute;
z-index: 10;
top: calc(100% + 7px);
right: 0;
min-width: 190px;
padding: 8px;
border: 1px solid var(--vd-border, #ddd);
border-radius: var(--vd-radius-md, 12px);
background: var(--vd-surface, #fff);
box-shadow: var(--vd-shadow-md, 0 14px 38px rgba(26, 23, 20, .14));
}
#inventory.ux-inventory-ready .ux-inventory-more-actions-body button {
width: 100%;
}
#inventory.ux-inventory-ready .ux-inventory-detail-toolbar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 14px;
padding: 14px;
border: 1px solid var(--vd-border, #ddd);
border-radius: var(--vd-radius-lg, 16px);
background: var(--vd-surface, #fff);
}
#inventory.ux-inventory-ready .ux-inventory-detail-search {
flex: 1 1 420px;
}
#inventory.ux-inventory-ready .ux-inventory-select-all {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 42px;
color: var(--vd-text-muted, #6f675f);
font-size: .82rem;
font-weight: 700;
}
#inventory.ux-inventory-ready .ux-inventory-selection-bar {
display: none;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
border: 1px solid color-mix(in srgb, var(--vd-accent, #d95f35) 35%, var(--vd-border, #ddd));
border-radius: var(--vd-radius-md, 12px);
background: color-mix(in srgb, var(--vd-accent, #d95f35) 7%, var(--vd-surface, #fff));
}
#inventory.ux-inventory-ready .ux-inventory-selection-bar.has-selection {
display: flex;
}
#inventory.ux-inventory-ready #inv-detail-list {
display: grid;
gap: 9px;
}
#inventory.ux-inventory-ready #inv-detail-list .history-item {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 12px;
align-items: center;
padding: 14px;
border: 1px solid var(--vd-border, #ddd);
border-radius: var(--vd-radius-md, 12px);
background: var(--vd-surface, #fff);
cursor: pointer;
}
#inventory.ux-inventory-ready #inv-detail-list .history-item.ux-inventory-selected {
border-color: color-mix(in srgb, var(--vd-accent, #d95f35) 48%, var(--vd-border, #ddd));
background: color-mix(in srgb, var(--vd-accent, #d95f35) 6%, var(--vd-surface, #fff));
}
#inventory.ux-inventory-ready #inv-detail-list .listing-title {
overflow-wrap: anywhere;
font-weight: 800;
}
#inventory.ux-inventory-ready #inv-detail-list .listing-meta {
margin-top: 4px;
color: var(--vd-text-muted, #6f675f);
font-size: .8rem;
}
#inventory.ux-inventory-ready #inv-detail-list .status-dot {
display: inline-flex;
padding: 3px 7px;
border-radius: 999px;
background: color-mix(in srgb, var(--vd-success, #16835d) 11%, var(--vd-surface, #fff));
color: var(--vd-success-strong, #126448);
font-size: .72rem;
font-weight: 800;
}
#inventory.ux-inventory-ready #inv-detail-list .status-dot.sold {
background: var(--vd-surface-subtle, #f4f1ed);
color: var(--vd-text-muted, #6f675f);
}
#inventory.ux-inventory-ready #inv-detail-list .status-dot.reserved {
background: color-mix(in srgb, var(--vd-warning, #b7791f) 12%, var(--vd-surface, #fff));
color: var(--vd-warning-strong, #7a4d08);
}
#inventory.ux-inventory-ready :is(button, input, select, summary, .inv-location-card):focus-visible {
outline: 3px solid color-mix(in srgb, var(--vd-accent, #d95f35) 48%, transparent);
outline-offset: 2px;
}
@media (max-width: 980px) {
#inventory.ux-inventory-ready .ux-inventory-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#inventory.ux-inventory-ready .ux-inventory-toolbar-primary,
#inventory.ux-inventory-ready .ux-inventory-controls {
align-items: stretch;
}
}
@media (max-width: 700px) {
#inventory.ux-inventory-ready .inventory-header,
#inventory.ux-inventory-ready .ux-inventory-overview-head,
#inventory.ux-inventory-ready .ux-inventory-toolbar-primary,
#inventory.ux-inventory-ready .ux-inventory-controls,
#inventory.ux-inventory-ready .ux-inventory-detail-head,
#inventory.ux-inventory-ready .ux-inventory-detail-toolbar {
flex-direction: column;
align-items: stretch;
}
#inventory.ux-inventory-ready .ux-inventory-next,
#inventory.ux-inventory-ready .inventory-actions-bar button,
#inventory.ux-inventory-ready .ux-inventory-search,
#inventory.ux-inventory-ready .ux-inventory-detail-search {
max-width: none;
width: 100%;
}
#inventory.ux-inventory-ready .ux-inventory-visible-count {
padding-bottom: 0;
}
#inventory.ux-inventory-ready .ux-inventory-sort select {
width: 100%;
}
#inventory.ux-inventory-ready .ux-inventory-more-actions-body {
right: auto;
left: 0;
}
}
@media (max-width: 470px) {
#inventory.ux-inventory-ready .ux-inventory-metrics,
#inventory.ux-inventory-ready .inventory-locations {
grid-template-columns: 1fr;
}
#inventory.ux-inventory-ready .ux-inventory-filter-group {
display: grid;
grid-template-columns: 1fr;
}
#inventory.ux-inventory-ready .ux-inventory-filter {
width: 100%;
}
#inventory.ux-inventory-ready .ux-inventory-selection-bar.has-selection {
align-items: stretch;
flex-direction: column;
}
#inventory.ux-inventory-ready .ux-inventory-selection-bar button {
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
#inventory.ux-inventory-ready *,
#inventory.ux-inventory-ready *::before,
#inventory.ux-inventory-ready *::after {
scroll-behavior: auto !important;
transition-duration: .01ms !important;
animation-duration: .01ms !important;
}
}
+380
View File
@@ -0,0 +1,380 @@
(() => {
'use strict';
const ROOT_ID = 'inventory';
const SORT_KEY = 'vendoo_inventory_sort';
const createNode = (tag, className = '', text = '') => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text) node.textContent = text;
return node;
};
const normalize = value => String(value || '').trim().toLocaleLowerCase('de-DE');
const parseNumber = value => {
const match = String(value ?? '').replace(/\./g, '').match(/-?[\d,]+/);
return match ? Number(match[0].replace(',', '.')) || 0 : 0;
};
function readStat(root, label) {
const card = [...root.querySelectorAll('#inv-stats .inv-stat-card')]
.find(item => normalize(item.querySelector('.inv-stat-label')?.textContent) === normalize(label));
return card?.querySelector('.inv-stat-value')?.textContent?.trim() || '0';
}
function locationMeta(card) {
return {
unassigned: card.classList.contains('inv-unassigned'),
count: parseNumber(card.querySelector('.inv-loc-count')?.textContent),
value: parseNumber(card.querySelector('.inv-loc-value')?.textContent),
name: card.querySelector('.inv-loc-name')?.textContent?.trim() || '',
};
}
function createMetric(key, label, copy) {
const item = createNode('article', 'ux-inventory-metric');
const value = createNode('strong', 'ux-inventory-metric-value', '0');
value.dataset.uxInventoryMetric = key;
const body = createNode('span', 'ux-inventory-metric-copy');
body.append(createNode('b', '', label), createNode('small', '', copy));
item.append(value, body);
return item;
}
function createOverview(root) {
const header = root.querySelector('.inventory-header');
const stats = root.querySelector('#inv-stats');
if (!header || !stats || root.querySelector('.ux-inventory-overview')) return;
const overview = createNode('section', 'ux-inventory-overview');
overview.setAttribute('aria-label', 'Bestandsübersicht');
const head = createNode('div', 'ux-inventory-overview-head');
const copy = createNode('div');
copy.append(
createNode('span', 'ux-inventory-kicker', 'Bestand im Blick'),
createNode('h3', '', 'Was braucht jetzt Aufmerksamkeit?'),
createNode('p', '', 'Nicht zugewiesene Artikel zuerst einordnen, danach Lagerorte durchsuchen und Bestände gezielt bearbeiten.')
);
const next = createNode('button', 'vd-button primary ux-inventory-next', 'Bestand öffnen');
next.type = 'button';
next.dataset.uxInventoryNext = 'overview';
head.append(copy, next);
const metrics = createNode('div', 'ux-inventory-metrics');
metrics.append(
createMetric('unassigned', 'Ohne Lagerort', 'Diese Artikel zuerst zuweisen'),
createMetric('locations', 'Lagerorte', 'Strukturierte Ablageplätze'),
createMetric('active', 'Aktiv', 'Aktuell im Verkaufsbestand'),
createMetric('value', 'Lagerwert', 'Summe der aktiven Artikel')
);
overview.append(head, metrics);
header.after(overview);
stats.classList.add('ux-inventory-stats-secondary');
next.addEventListener('click', () => {
const target = root.querySelector('#inv-locations .inv-unassigned:not(.ux-inventory-filtered-out)')
|| root.querySelector('#inv-locations .inv-location-card:not(.ux-inventory-filtered-out)');
if (!target) return;
if (target.classList.contains('inv-unassigned')) target.click();
else {
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
target.focus({ preventScroll: true });
}
});
}
function createLocationToolbar(root) {
const locations = root.querySelector('#inv-locations');
if (!locations || root.querySelector('.ux-inventory-toolbar')) return;
const toolbar = createNode('section', 'ux-inventory-toolbar card');
const primary = createNode('div', 'ux-inventory-toolbar-primary');
const searchLabel = createNode('label', 'ux-inventory-search');
searchLabel.append(createNode('span', '', 'Lagerort suchen'));
const search = createNode('input');
search.type = 'search';
search.placeholder = 'Regal, Karton oder Schrank …';
search.dataset.uxInventorySearch = 'true';
searchLabel.append(search);
const count = createNode('span', 'ux-inventory-visible-count', '0 Lagerorte');
count.setAttribute('aria-live', 'polite');
primary.append(searchLabel, count);
const controls = createNode('div', 'ux-inventory-controls');
const filters = createNode('div', 'ux-inventory-filter-group');
filters.setAttribute('role', 'group');
filters.setAttribute('aria-label', 'Lagerorte filtern');
[['all', 'Alle'], ['assigned', 'Zugewiesen'], ['unassigned', 'Ohne Lagerort']].forEach(([value, label]) => {
const button = createNode('button', 'ux-inventory-filter', label);
button.type = 'button';
button.dataset.uxInventoryFilter = value;
button.setAttribute('aria-pressed', value === 'all' ? 'true' : 'false');
filters.append(button);
});
const sortLabel = createNode('label', 'ux-inventory-sort');
sortLabel.append(createNode('span', '', 'Sortierung'));
const sort = createNode('select');
sort.dataset.uxInventorySort = 'true';
[['attention', 'Aufmerksamkeit zuerst'], ['name', 'Name AZ'], ['items', 'Meiste Artikel'], ['value', 'Höchster Wert']]
.forEach(([value, label]) => {
const option = createNode('option', '', label);
option.value = value;
sort.append(option);
});
try { sort.value = localStorage.getItem(SORT_KEY) || 'attention'; } catch {}
sortLabel.append(sort);
controls.append(filters, sortLabel);
toolbar.append(primary, controls);
locations.before(toolbar);
}
function enhanceLocationCards(root) {
root.querySelectorAll('#inv-locations .inv-location-card').forEach(card => {
const meta = locationMeta(card);
card.dataset.uxInventoryKind = meta.unassigned ? 'unassigned' : 'assigned';
card.dataset.uxInventoryCount = String(meta.count);
card.dataset.uxInventoryValue = String(meta.value);
card.dataset.uxInventoryName = normalize(meta.name);
card.classList.toggle('ux-inventory-needs-attention', meta.unassigned && meta.count > 0);
card.setAttribute('role', 'button');
card.tabIndex = 0;
card.setAttribute('aria-label', `${meta.name || 'Lagerort'} öffnen, ${meta.count} Artikel`);
if (card.dataset.uxInventoryKeyboard === 'true') return;
card.dataset.uxInventoryKeyboard = 'true';
card.addEventListener('keydown', event => {
if (!['Enter', ' '].includes(event.key)) return;
event.preventDefault();
card.click();
});
});
}
function sortLocationCards(root) {
const container = root.querySelector('#inv-locations');
if (!container) return;
const mode = root.querySelector('[data-ux-inventory-sort]')?.value || 'attention';
const current = [...container.querySelectorAll('.inv-location-card')];
const sorted = [...current].sort((a, b) => {
const left = locationMeta(a);
const right = locationMeta(b);
if (mode === 'name') return left.name.localeCompare(right.name, 'de');
if (mode === 'items') return right.count - left.count || left.name.localeCompare(right.name, 'de');
if (mode === 'value') return right.value - left.value || left.name.localeCompare(right.name, 'de');
if (left.unassigned !== right.unassigned) return left.unassigned ? -1 : 1;
return right.count - left.count || left.name.localeCompare(right.name, 'de');
});
if (!sorted.some((card, index) => current[index] !== card)) return;
sorted.forEach(card => container.append(card));
}
function applyLocationFilters(root) {
const query = normalize(root.querySelector('[data-ux-inventory-search]')?.value);
const filter = root.querySelector('[data-ux-inventory-filter][aria-pressed="true"]')?.dataset.uxInventoryFilter || 'all';
let visible = 0;
root.querySelectorAll('#inv-locations .inv-location-card').forEach(card => {
const meta = locationMeta(card);
const show = (!query || normalize(meta.name).includes(query))
&& (filter === 'all' || (filter === 'assigned' && !meta.unassigned) || (filter === 'unassigned' && meta.unassigned));
card.classList.toggle('ux-inventory-filtered-out', !show);
if (show) visible += 1;
});
const count = root.querySelector('.ux-inventory-visible-count');
if (count) count.textContent = `${visible} ${visible === 1 ? 'Lagerort' : 'Lagerorte'}`;
}
function updateOverview(root) {
const unassignedCard = root.querySelector('#inv-locations .inv-unassigned');
const unassigned = unassignedCard ? locationMeta(unassignedCard).count : 0;
const values = {
unassigned: String(unassigned),
locations: readStat(root, 'Lagerorte'),
active: readStat(root, 'Aktive Artikel'),
value: readStat(root, 'Lagerwert'),
};
Object.entries(values).forEach(([key, value]) => {
const node = root.querySelector(`[data-ux-inventory-metric="${key}"]`);
if (node && node.textContent !== value) node.textContent = value;
});
const next = root.querySelector('[data-ux-inventory-next]');
if (next) {
const label = unassigned > 0 ? `${unassigned} Artikel ohne Lagerort zuweisen` : 'Lagerorte durchsuchen';
if (next.textContent !== label) next.textContent = label;
next.dataset.uxInventoryNext = unassigned > 0 ? 'unassigned' : 'overview';
}
root.classList.toggle('ux-inventory-has-unassigned', unassigned > 0);
}
function createDetailWorkspace(root) {
const detail = root.querySelector('#inv-detail');
const list = root.querySelector('#inv-detail-list');
const actions = detail?.querySelector('.inventory-detail-actions');
if (!detail || !list || !actions || detail.dataset.uxInventoryStructured === 'true') return;
const head = createNode('div', 'ux-inventory-detail-head');
const titleWrap = createNode('div', 'ux-inventory-detail-title-wrap');
const back = detail.querySelector('#inv-back');
const title = detail.querySelector('#inv-detail-title');
if (back) titleWrap.append(back);
if (title) titleWrap.append(title);
const more = createNode('details', 'ux-inventory-more-actions');
const moreBody = createNode('div', 'ux-inventory-more-actions-body');
const rename = detail.querySelector('#inv-rename-btn');
if (rename) moreBody.append(rename);
more.append(createNode('summary', '', 'Lagerort-Aktionen'), moreBody);
head.append(titleWrap, more);
const toolbar = createNode('div', 'ux-inventory-detail-toolbar');
const searchLabel = createNode('label', 'ux-inventory-detail-search');
searchLabel.append(createNode('span', '', 'Artikel suchen'));
const search = createNode('input');
search.type = 'search';
search.placeholder = 'Titel, SKU, Plattform oder Status …';
search.dataset.uxInventoryDetailSearch = 'true';
searchLabel.append(search);
const selectLabel = createNode('label', 'ux-inventory-select-all');
const selectAll = createNode('input');
selectAll.type = 'checkbox';
selectAll.dataset.uxInventorySelectAll = 'true';
selectLabel.append(selectAll, createNode('span', '', 'Sichtbare auswählen'));
toolbar.append(searchLabel, selectLabel);
const selection = createNode('div', 'ux-inventory-selection-bar');
selection.setAttribute('aria-live', 'polite');
const selectedCount = createNode('strong', 'ux-inventory-selected-count', '0 ausgewählt');
const move = detail.querySelector('#inv-move-btn');
selection.append(selectedCount);
if (move) selection.append(move);
actions.remove();
detail.prepend(head);
list.before(toolbar, selection);
detail.dataset.uxInventoryStructured = 'true';
search.addEventListener('input', () => applyDetailFilter(root));
selectAll.addEventListener('change', () => {
root.querySelectorAll('#inv-detail-list .history-item:not(.ux-inventory-detail-filtered) .inv-item-check').forEach(checkbox => {
checkbox.checked = selectAll.checked;
checkbox.closest('.history-item')?.classList.toggle('ux-inventory-selected', checkbox.checked);
});
updateDetailSelection(root);
});
}
function enhanceDetailRows(root) {
root.querySelectorAll('#inv-detail-list .history-item').forEach(row => {
const checkbox = row.querySelector('.inv-item-check');
const status = row.querySelector('.status-dot');
if (status && row.dataset.uxInventoryStatusReady !== 'true') {
const raw = normalize(status.textContent);
status.textContent = raw === 'sold' ? 'Verkauft' : raw === 'reserved' ? 'Reserviert' : 'Aktiv';
row.dataset.uxInventoryStatus = raw || 'active';
row.dataset.uxInventoryStatusReady = 'true';
}
if (row.dataset.uxInventoryBound === 'true') return;
row.dataset.uxInventoryBound = 'true';
checkbox?.addEventListener('change', () => {
row.classList.toggle('ux-inventory-selected', checkbox.checked);
updateDetailSelection(root);
});
row.addEventListener('click', event => {
if (event.target.closest('input,button,a,select') || !checkbox) return;
checkbox.checked = !checkbox.checked;
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
});
});
applyDetailFilter(root);
updateDetailSelection(root);
}
function applyDetailFilter(root) {
const query = normalize(root.querySelector('[data-ux-inventory-detail-search]')?.value);
root.querySelectorAll('#inv-detail-list .history-item').forEach(row => {
row.classList.toggle('ux-inventory-detail-filtered', Boolean(query) && !normalize(row.textContent).includes(query));
});
updateDetailSelection(root);
}
function updateDetailSelection(root) {
const visible = [...root.querySelectorAll('#inv-detail-list .history-item:not(.ux-inventory-detail-filtered) .inv-item-check')];
const selected = [...root.querySelectorAll('#inv-detail-list .inv-item-check:checked')];
const count = root.querySelector('.ux-inventory-selected-count');
if (count) count.textContent = `${selected.length} ausgewählt`;
root.querySelector('.ux-inventory-selection-bar')?.classList.toggle('has-selection', selected.length > 0);
const move = root.querySelector('#inv-move-btn');
if (move) move.disabled = selected.length === 0;
const selectAll = root.querySelector('[data-ux-inventory-select-all]');
if (selectAll) {
const selectedVisible = visible.filter(item => item.checked).length;
selectAll.checked = visible.length > 0 && selectedVisible === visible.length;
selectAll.indeterminate = selectedVisible > 0 && selectedVisible < visible.length;
}
}
function bindToolbar(root) {
root.querySelector('[data-ux-inventory-search]')?.addEventListener('input', () => applyLocationFilters(root));
root.querySelectorAll('[data-ux-inventory-filter]').forEach(button => {
button.addEventListener('click', () => {
root.querySelectorAll('[data-ux-inventory-filter]').forEach(item => item.setAttribute('aria-pressed', String(item === button)));
applyLocationFilters(root);
});
button.addEventListener('keydown', event => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
event.preventDefault();
const buttons = [...root.querySelectorAll('[data-ux-inventory-filter]')];
const index = buttons.indexOf(button);
const nextIndex = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1
: event.key === 'ArrowRight' ? (index + 1) % buttons.length : (index - 1 + buttons.length) % buttons.length;
buttons[nextIndex]?.focus();
});
});
root.querySelector('[data-ux-inventory-sort]')?.addEventListener('change', event => {
try { localStorage.setItem(SORT_KEY, event.target.value); } catch {}
sortLocationCards(root);
applyLocationFilters(root);
});
}
function refresh(root) {
enhanceLocationCards(root);
sortLocationCards(root);
applyLocationFilters(root);
updateOverview(root);
enhanceDetailRows(root);
}
function observe(root) {
let scheduled = false;
const schedule = () => {
if (scheduled) return;
scheduled = true;
window.requestAnimationFrame(() => {
scheduled = false;
refresh(root);
});
};
const observer = new MutationObserver(schedule);
[root.querySelector('#inv-stats'), root.querySelector('#inv-locations'), root.querySelector('#inv-detail-list')]
.filter(Boolean)
.forEach(node => observer.observe(node, { subtree: true, childList: true, characterData: true }));
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
function init() {
const root = document.getElementById(ROOT_ID);
if (!root || root.dataset.uxInventoryReady === 'true') return;
root.dataset.uxInventoryReady = 'true';
root.classList.add('ux-inventory-ready');
createOverview(root);
createLocationToolbar(root);
createDetailWorkspace(root);
bindToolbar(root);
refresh(root);
observe(root);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true });
else init();
})();
+695
View File
@@ -0,0 +1,695 @@
/* Vendoo 1.44.0 — additive media library UX layer */
#media-library.media-library-page {
--ux-media-card-min: 210px;
--ux-media-image-ratio: 4 / 3;
display: block;
}
#media-library[data-media-density="compact"] {
--ux-media-card-min: 168px;
--ux-media-image-ratio: 1 / 1;
}
#media-library[data-media-density="large"] {
--ux-media-card-min: 280px;
--ux-media-image-ratio: 4 / 3;
}
.ux-media-command {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
margin: 18px 0 12px;
padding: 16px 18px;
border: 1px solid var(--vd-border, rgba(148, 163, 184, .24));
border-radius: var(--vd-radius-lg, 18px);
background: var(--vd-surface, #fff);
box-shadow: var(--vd-shadow-sm, 0 10px 30px rgba(15, 23, 42, .06));
}
.ux-media-command-copy {
min-width: 0;
}
.ux-media-command-copy > span {
display: block;
margin-bottom: 3px;
color: var(--vd-text-muted, #64748b);
font-size: .75rem;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
}
.ux-media-command-copy strong {
display: block;
color: var(--vd-text, #0f172a);
font-size: 1rem;
}
.ux-media-command-copy small {
display: block;
margin-top: 3px;
color: var(--vd-text-muted, #64748b);
line-height: 1.45;
}
.ux-media-density {
display: inline-flex;
flex: 0 0 auto;
gap: 4px;
padding: 4px;
border: 1px solid var(--vd-border, rgba(148, 163, 184, .24));
border-radius: 12px;
background: var(--vd-surface-muted, rgba(148, 163, 184, .09));
}
.ux-media-density button {
min-width: 38px;
min-height: 34px;
padding: 0 10px;
border: 0;
border-radius: 9px;
background: transparent;
color: var(--vd-text-muted, #64748b);
cursor: pointer;
font: inherit;
font-size: .78rem;
font-weight: 700;
}
.ux-media-density button.is-active {
background: var(--vd-surface, #fff);
color: var(--vd-accent, #2563eb);
box-shadow: 0 2px 10px rgba(15, 23, 42, .09);
}
#media-library .media-summary {
display: flex;
gap: 8px;
overflow-x: auto;
padding: 3px 2px 8px;
scrollbar-width: thin;
}
#media-library .media-summary > button {
flex: 0 0 auto;
min-width: 112px;
}
#media-library .media-toolbar {
display: grid;
grid-template-columns: minmax(260px, 1fr) auto auto;
align-items: center;
gap: 10px;
padding: 12px;
border: 1px solid var(--vd-border, rgba(148, 163, 184, .24));
border-radius: var(--vd-radius-lg, 18px);
background: var(--vd-surface, #fff);
}
#media-library .media-search {
min-width: 0;
}
.ux-media-filter-details {
position: relative;
}
.ux-media-filter-details > summary {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 40px;
padding: 0 13px;
border: 1px solid var(--vd-border, rgba(148, 163, 184, .3));
border-radius: 11px;
color: var(--vd-text, #0f172a);
cursor: pointer;
font-size: .86rem;
font-weight: 700;
list-style: none;
user-select: none;
}
.ux-media-filter-details > summary::-webkit-details-marker {
display: none;
}
.ux-media-filter-details > summary::after {
content: "⌄";
color: var(--vd-text-muted, #64748b);
}
.ux-media-filter-details[open] > summary,
.ux-media-filter-details > summary:hover {
border-color: color-mix(in srgb, var(--vd-accent, #2563eb) 42%, transparent);
background: color-mix(in srgb, var(--vd-accent, #2563eb) 7%, transparent);
}
.ux-media-filter-panel {
position: absolute;
z-index: 40;
top: calc(100% + 8px);
right: 0;
display: grid;
gap: 12px;
width: min(350px, calc(100vw - 40px));
padding: 15px;
border: 1px solid var(--vd-border, rgba(148, 163, 184, .28));
border-radius: 14px;
background: var(--vd-surface, #fff);
box-shadow: var(--vd-shadow-lg, 0 22px 55px rgba(15, 23, 42, .18));
}
.ux-media-filter-panel > label,
.ux-media-filter-panel > .ux-media-filter-field {
display: grid;
gap: 6px;
color: var(--vd-text-muted, #64748b);
font-size: .78rem;
font-weight: 700;
}
.ux-media-filter-panel select {
width: 100%;
}
.ux-media-filter-panel .media-check {
display: flex;
align-items: center;
gap: 9px;
min-height: 40px;
padding: 0 11px;
border-radius: 10px;
background: var(--vd-surface-muted, rgba(148, 163, 184, .09));
color: var(--vd-text, #0f172a);
}
.ux-media-active-filters {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 7px;
min-height: 0;
margin: 9px 0 0;
}
.ux-media-active-filters.is-empty {
display: none;
}
.ux-media-active-filters > strong {
color: var(--vd-text-muted, #64748b);
font-size: .76rem;
}
.ux-media-filter-chip {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 30px;
padding: 0 5px 0 10px;
border: 1px solid color-mix(in srgb, var(--vd-accent, #2563eb) 24%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--vd-accent, #2563eb) 7%, transparent);
color: var(--vd-text, #0f172a);
font-size: .77rem;
font-weight: 700;
}
.ux-media-filter-chip button,
.ux-media-filter-reset {
border: 0;
background: transparent;
color: inherit;
cursor: pointer;
font: inherit;
}
.ux-media-filter-chip button {
display: grid;
width: 23px;
height: 23px;
place-items: center;
border-radius: 50%;
}
.ux-media-filter-chip button:hover {
background: rgba(15, 23, 42, .09);
}
.ux-media-filter-reset {
color: var(--vd-accent, #2563eb);
font-size: .77rem;
font-weight: 700;
}
#media-library .media-batchbar {
display: none;
position: sticky;
z-index: 28;
top: 72px;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 12px 0;
padding: 10px 12px;
border: 1px solid color-mix(in srgb, var(--vd-accent, #2563eb) 30%, transparent);
border-radius: 14px;
background: color-mix(in srgb, var(--vd-surface, #fff) 94%, var(--vd-accent, #2563eb));
box-shadow: 0 12px 30px rgba(15, 23, 42, .12);
}
#media-library[data-has-media-selection] .media-batchbar {
display: flex;
}
.ux-media-batch-primary {
display: flex;
flex-wrap: wrap;
gap: 7px;
}
.ux-media-batch-more {
position: relative;
}
.ux-media-batch-more > summary {
display: inline-flex;
align-items: center;
min-height: 34px;
padding: 0 10px;
border: 1px solid var(--vd-border, rgba(148, 163, 184, .28));
border-radius: 9px;
cursor: pointer;
font-size: .78rem;
font-weight: 700;
list-style: none;
}
.ux-media-batch-more > summary::-webkit-details-marker {
display: none;
}
.ux-media-batch-secondary {
position: absolute;
z-index: 42;
top: calc(100% + 7px);
right: 0;
min-width: 190px;
padding: 8px;
border: 1px solid var(--vd-border, rgba(148, 163, 184, .28));
border-radius: 12px;
background: var(--vd-surface, #fff);
box-shadow: var(--vd-shadow-lg, 0 20px 45px rgba(15, 23, 42, .18));
}
.ux-media-batch-secondary button {
width: 100%;
}
#media-library .media-pagination-top.is-redundant {
display: none;
}
#media-library .media-pagination {
margin: 14px 0;
}
#media-library .media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--ux-media-card-min)), 1fr));
gap: 14px;
align-items: start;
}
#media-library .media-card {
position: relative;
min-width: 0;
overflow: hidden;
border: 1px solid var(--vd-border, rgba(148, 163, 184, .24));
border-radius: 15px;
background: var(--vd-surface, #fff);
box-shadow: 0 5px 18px rgba(15, 23, 42, .055);
transition: border-color .16s ease, box-shadow .16s ease, transform .16s ease;
}
#media-library .media-card:hover,
#media-library .media-card:focus-within {
border-color: color-mix(in srgb, var(--vd-accent, #2563eb) 38%, transparent);
box-shadow: 0 14px 34px rgba(15, 23, 42, .12);
transform: translateY(-2px);
}
#media-library .media-card.is-selected {
border-color: var(--vd-accent, #2563eb);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--vd-accent, #2563eb) 20%, transparent), 0 14px 34px rgba(15, 23, 42, .12);
}
#media-library .media-card-image {
position: relative;
aspect-ratio: var(--ux-media-image-ratio);
overflow: hidden;
background: var(--vd-surface-muted, #eef2f7);
}
#media-library .media-card-open {
display: block;
width: 100%;
height: 100%;
padding: 0;
border: 0;
background: transparent;
cursor: zoom-in;
}
#media-library .media-card-open img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform .25s ease;
}
#media-library .media-card:hover .media-card-open img,
#media-library .media-card:focus-within .media-card-open img {
transform: scale(1.025);
}
#media-library .media-open-indicator {
position: absolute;
right: 10px;
bottom: 10px;
display: grid;
width: 34px;
height: 34px;
place-items: center;
border: 1px solid rgba(255, 255, 255, .7);
border-radius: 50%;
background: rgba(15, 23, 42, .72);
color: #fff;
opacity: 0;
transform: translateY(4px);
transition: opacity .16s ease, transform .16s ease;
}
#media-library .media-card:hover .media-open-indicator,
#media-library .media-card:focus-within .media-open-indicator {
opacity: 1;
transform: translateY(0);
}
#media-library .media-card-select,
#media-library .media-card-favorite-toggle {
width: 30px;
height: 30px;
border-radius: 9px;
box-shadow: 0 4px 14px rgba(15, 23, 42, .16);
}
#media-library .media-card-select {
top: 9px;
left: 9px;
}
#media-library .media-card-favorite-toggle {
top: 9px;
right: 9px;
}
#media-library .media-card-content {
display: grid;
gap: 5px;
padding: 12px 12px 9px;
}
#media-library .media-card-content > strong {
overflow: hidden;
color: var(--vd-text, #0f172a);
text-overflow: ellipsis;
white-space: nowrap;
}
#media-library .media-card-content > span,
#media-library .media-card-content > small {
overflow: hidden;
color: var(--vd-text-muted, #64748b);
text-overflow: ellipsis;
white-space: nowrap;
}
#media-library .media-card-sources {
display: flex;
min-height: 21px;
gap: 5px;
overflow: hidden;
}
#media-library .media-card-actions {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 3px;
padding: 6px;
border-top: 1px solid var(--vd-border, rgba(148, 163, 184, .18));
background: var(--vd-surface-muted, rgba(148, 163, 184, .055));
}
#media-library .media-action {
display: inline-grid;
min-width: 0;
min-height: 36px;
place-items: center;
padding: 0;
border-radius: 9px;
}
#media-library .media-action-label {
position: absolute !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
clip: rect(0 0 0 0) !important;
white-space: nowrap !important;
}
#media-library .media-grid.is-list {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
}
#media-library .media-grid.is-list .media-card {
display: grid;
grid-template-columns: 116px minmax(0, 1fr) auto;
align-items: stretch;
}
#media-library .media-grid.is-list .media-card-image {
aspect-ratio: 4 / 3;
min-height: 92px;
}
#media-library .media-grid.is-list .media-card-actions {
grid-template-columns: repeat(2, 42px);
align-content: center;
border-top: 0;
border-left: 1px solid var(--vd-border, rgba(148, 163, 184, .18));
}
.ux-media-lightbox-nav {
display: inline-flex;
align-items: center;
gap: 5px;
margin-left: auto;
}
.ux-media-lightbox-position {
min-width: 72px;
color: var(--vd-text-muted, #64748b);
font-size: .78rem;
text-align: center;
}
#media-lightbox .media-lightbox-dialog > header {
gap: 12px;
}
#media-lightbox .media-lightbox-body {
min-height: min(68vh, 760px);
}
#media-lightbox .media-lightbox-stage {
min-width: 0;
}
#media-lightbox .media-lightbox-dialog > footer {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
#media-lightbox .ux-media-use {
order: -1;
}
#media-lightbox .media-lightbox-meta-details {
display: contents;
}
#media-lightbox .media-lightbox-meta-details > summary {
display: none;
}
@media (max-width: 1100px) {
#media-library .media-toolbar {
grid-template-columns: minmax(220px, 1fr) auto;
}
#media-library .media-view-switch {
grid-column: 2;
}
#media-lightbox .media-lightbox-body {
grid-template-columns: 1fr;
}
#media-lightbox .media-lightbox-meta-details {
display: block;
padding: 0 14px 14px;
}
#media-lightbox .media-lightbox-meta-details > summary {
display: block;
padding: 11px 12px;
border: 1px solid var(--vd-border, rgba(148, 163, 184, .24));
border-radius: 11px;
cursor: pointer;
font-weight: 700;
}
#media-lightbox .media-lightbox-meta-details > #media-lightbox-meta {
margin-top: 10px;
}
}
@media (max-width: 760px) {
.ux-media-command,
#media-library .media-toolbar,
#media-library .media-batchbar {
align-items: stretch;
}
.ux-media-command {
flex-direction: column;
}
.ux-media-density {
align-self: flex-start;
}
#media-library .media-toolbar {
grid-template-columns: 1fr auto;
}
#media-library .media-search {
grid-column: 1 / -1;
}
.ux-media-filter-details {
justify-self: start;
}
.ux-media-filter-panel {
right: auto;
left: 0;
}
#media-library .media-batchbar {
flex-direction: column;
}
.ux-media-batch-primary {
width: 100%;
}
.ux-media-batch-primary button {
flex: 1 1 140px;
}
#media-library .media-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 9px;
}
#media-library[data-media-density="large"] .media-grid {
grid-template-columns: 1fr;
}
#media-library .media-card-content {
padding: 10px;
}
#media-library .media-grid.is-list .media-card {
grid-template-columns: 92px minmax(0, 1fr);
}
#media-library .media-grid.is-list .media-card-actions {
grid-column: 1 / -1;
grid-template-columns: repeat(4, 1fr);
border-top: 1px solid var(--vd-border, rgba(148, 163, 184, .18));
border-left: 0;
}
#media-lightbox .media-lightbox-toolbar {
overflow-x: auto;
}
.ux-media-lightbox-nav {
width: 100%;
order: 2;
justify-content: center;
}
}
@media (max-width: 470px) {
#media-library .media-grid {
grid-template-columns: 1fr;
}
#media-library .media-summary > button {
min-width: 96px;
}
}
@media (hover: none) {
#media-library .media-open-indicator {
opacity: 1;
transform: none;
}
#media-library .media-card:hover {
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
#media-library .media-card,
#media-library .media-card-open img,
#media-library .media-open-indicator {
transition: none;
}
}
#media-library button:focus-visible,
#media-library a:focus-visible,
#media-library input:focus-visible,
#media-library select:focus-visible,
#media-lightbox button:focus-visible,
#media-lightbox a:focus-visible,
#media-lightbox input:focus-visible {
outline: 3px solid color-mix(in srgb, var(--vd-accent, #2563eb) 38%, transparent);
outline-offset: 2px;
}
+406
View File
@@ -0,0 +1,406 @@
(() => {
'use strict';
const DENSITY_KEY = 'vendoo_media_density_v1';
const DENSITIES = new Set(['compact', 'comfortable', 'large']);
let activePath = '';
let refreshFrame = 0;
let gridObserver = null;
let countObserver = null;
function root() {
return document.getElementById('media-library');
}
function createElement(tag, className, text) {
const element = document.createElement(tag);
if (className) element.className = className;
if (text !== undefined) element.textContent = text;
return element;
}
function queueRefresh() {
if (refreshFrame) return;
refreshFrame = requestAnimationFrame(() => {
refreshFrame = 0;
refreshPresentation();
});
}
function savedDensity() {
const value = localStorage.getItem(DENSITY_KEY) || 'comfortable';
return DENSITIES.has(value) ? value : 'comfortable';
}
function applyDensity(value) {
const density = DENSITIES.has(value) ? value : 'comfortable';
const section = root();
if (!section) return;
section.dataset.mediaDensity = density;
localStorage.setItem(DENSITY_KEY, density);
section.querySelectorAll('[data-media-density-option]').forEach(button => {
const active = button.dataset.mediaDensityOption === density;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', String(active));
});
}
function createCommandCenter() {
const section = root();
const summary = section?.querySelector('.media-summary');
if (!section || !summary || section.querySelector('.ux-media-command')) return;
const command = createElement('section', 'ux-media-command');
command.setAttribute('aria-label', 'Galerieansicht');
const copy = createElement('div', 'ux-media-command-copy');
const kicker = createElement('span', '', 'Arbeitsansicht');
const title = createElement('strong', '', 'Bilder schnell finden, prüfen und weiterverwenden');
const result = createElement('small', '', 'Die Galerie wird geladen …');
result.id = 'ux-media-result-summary';
copy.append(kicker, title, result);
const density = createElement('div', 'ux-media-density');
density.setAttribute('role', 'group');
density.setAttribute('aria-label', 'Kartengröße');
const options = [
['compact', 'Klein', 'Kompakte Karten'],
['comfortable', 'Mittel', 'Normale Karten'],
['large', 'Groß', 'Große Karten'],
];
for (const [value, label, ariaLabel] of options) {
const button = createElement('button', '', label);
button.type = 'button';
button.dataset.mediaDensityOption = value;
button.setAttribute('aria-label', ariaLabel);
button.addEventListener('click', () => applyDensity(value));
density.append(button);
}
command.append(copy, density);
summary.before(command);
applyDensity(savedDensity());
}
function wrapSelect(select, labelText) {
if (!select || select.closest('.ux-media-filter-field')) return null;
const label = createElement('label', 'ux-media-filter-field');
label.append(createElement('span', '', labelText), select);
return label;
}
function reorganizeToolbar() {
const toolbar = root()?.querySelector('.media-toolbar');
if (!toolbar || toolbar.querySelector('.ux-media-filter-details')) return;
const source = document.getElementById('media-source');
const sort = document.getElementById('media-sort');
const favorites = document.getElementById('media-favorites-only')?.closest('.media-check');
const pageSize = document.getElementById('media-page-size');
const details = createElement('details', 'ux-media-filter-details');
const summary = createElement('summary', '', 'Filter & Anzeige');
const panel = createElement('div', 'ux-media-filter-panel');
const sourceField = wrapSelect(source, 'Quelle');
const sortField = wrapSelect(sort, 'Sortierung');
const pageField = wrapSelect(pageSize, 'Bilder pro Seite');
[sourceField, sortField, favorites, pageField].filter(Boolean).forEach(node => panel.append(node));
details.append(summary, panel);
const viewSwitch = toolbar.querySelector('.media-view-switch');
if (viewSwitch) toolbar.insertBefore(details, viewSwitch);
else toolbar.append(details);
document.addEventListener('click', event => {
if (!details.open || details.contains(event.target)) return;
details.open = false;
});
}
function createActiveFilters() {
const toolbar = root()?.querySelector('.media-toolbar');
if (!toolbar || document.getElementById('ux-media-active-filters')) return;
const filters = createElement('div', 'ux-media-active-filters is-empty');
filters.id = 'ux-media-active-filters';
filters.setAttribute('aria-live', 'polite');
toolbar.after(filters);
}
function controlEvent(control) {
return control?.tagName === 'INPUT' && control.type === 'search' ? 'input' : 'change';
}
function resetControl(control, value) {
if (!control) return;
if (control.type === 'checkbox') control.checked = Boolean(value);
else control.value = value;
control.dispatchEvent(new Event(controlEvent(control), { bubbles: true }));
queueRefresh();
}
function filterChip(label, control, resetValue) {
const chip = createElement('span', 'ux-media-filter-chip');
chip.append(createElement('span', '', label));
const clear = createElement('button', '', '×');
clear.type = 'button';
clear.setAttribute('aria-label', `${label} entfernen`);
clear.addEventListener('click', () => resetControl(control, resetValue));
chip.append(clear);
return chip;
}
function selectedText(select) {
if (!select?.value) return '';
return select.options[select.selectedIndex]?.textContent?.trim() || select.value;
}
function updateActiveFilters() {
const container = document.getElementById('ux-media-active-filters');
if (!container) return;
const search = document.getElementById('media-search');
const source = document.getElementById('media-source');
const favorites = document.getElementById('media-favorites-only');
const chips = [];
if (search?.value.trim()) chips.push(filterChip(`Suche: ${search.value.trim()}`, search, ''));
if (source?.value && source.value !== 'all') chips.push(filterChip(`Quelle: ${selectedText(source)}`, source, 'all'));
if (favorites?.checked) chips.push(filterChip('Nur Favoriten', favorites, false));
container.replaceChildren();
container.classList.toggle('is-empty', chips.length === 0);
if (!chips.length) return;
container.append(createElement('strong', '', 'Aktive Filter'), ...chips);
const reset = createElement('button', 'ux-media-filter-reset', 'Alle entfernen');
reset.type = 'button';
reset.addEventListener('click', () => {
resetControl(search, '');
resetControl(source, 'all');
resetControl(favorites, false);
});
container.append(reset);
}
function enhanceBatchBar() {
const bar = root()?.querySelector('.media-batchbar');
if (!bar || bar.querySelector('.ux-media-batch-primary')) return;
const actions = bar.querySelector(':scope > div');
if (!actions) return;
const primary = createElement('div', 'ux-media-batch-primary');
const more = createElement('details', 'ux-media-batch-more');
const summary = createElement('summary', '', 'Weitere Aktionen');
const secondary = createElement('div', 'ux-media-batch-secondary');
for (const id of ['media-send-to-factory', 'media-download-selected']) {
const button = document.getElementById(id);
if (button) primary.append(button);
}
const remove = document.getElementById('media-delete-selected');
if (remove) secondary.append(remove);
more.append(summary, secondary);
actions.replaceChildren(primary, more);
}
function selectedCount() {
const value = document.getElementById('media-selected-count')?.textContent || '0';
return Number.parseInt(value, 10) || 0;
}
function updateSelectionState() {
const section = root();
if (!section) return;
const count = selectedCount();
section.toggleAttribute('data-has-media-selection', count > 0);
const more = section.querySelector('.ux-media-batch-more');
if (!count && more) more.open = false;
}
function annotateCards() {
const cards = root()?.querySelectorAll('#media-grid .media-card') || [];
cards.forEach(card => {
const preview = card.querySelector('[data-media-preview]');
const title = card.querySelector('.media-card-content > strong')?.textContent?.trim() || 'Bild';
if (preview) {
preview.setAttribute('title', `${title} groß öffnen`);
preview.setAttribute('aria-label', `${title} in großer Vorschau öffnen`);
}
card.querySelectorAll('.media-action').forEach(action => {
const label = action.querySelector('.media-action-label')?.textContent?.trim();
if (label && !action.getAttribute('aria-label')) action.setAttribute('aria-label', label);
});
});
}
function updateResultSummary() {
const target = document.getElementById('ux-media-result-summary');
if (!target) return;
const summary = root()?.querySelector('.media-pagination-summary')?.textContent?.trim();
const view = document.getElementById('media-view-list')?.classList.contains('active') ? 'Listenansicht' : 'Rasteransicht';
target.textContent = `${summary || '0 Bilder'} · ${view}`;
}
function updatePaginationState() {
const section = root();
if (!section) return;
const pageInfo = section.querySelector('[data-media-page-info]')?.textContent?.trim() || '';
const onePage = /von\s+1\b/i.test(pageInfo);
section.querySelector('.media-pagination-top')?.classList.toggle('is-redundant', onePage);
}
function previewButtons() {
return [...(root()?.querySelectorAll('[data-media-preview]') || [])];
}
function currentPreviewIndex() {
const buttons = previewButtons();
return buttons.findIndex(button => button.dataset.mediaPreview === activePath);
}
function navigatePreview(direction) {
const buttons = previewButtons();
if (!buttons.length) return;
const current = currentPreviewIndex();
const next = current < 0 ? 0 : (current + direction + buttons.length) % buttons.length;
activePath = buttons[next].dataset.mediaPreview || '';
buttons[next].click();
queueRefresh();
}
function updateLightboxNavigation() {
const modal = document.getElementById('media-lightbox');
const buttons = previewButtons();
const index = currentPreviewIndex();
const position = document.getElementById('ux-media-lightbox-position');
if (position) position.textContent = index >= 0 ? `${index + 1} von ${buttons.length}` : `${buttons.length} Bilder`;
modal?.querySelectorAll('[data-ux-media-preview-nav]').forEach(button => {
button.disabled = buttons.length < 2;
});
}
function activeCard() {
if (!activePath) return null;
return previewButtons().find(button => button.dataset.mediaPreview === activePath)?.closest('.media-card') || null;
}
function enhanceLightbox() {
const modal = document.getElementById('media-lightbox');
const dialog = modal?.querySelector('.media-lightbox-dialog');
const header = dialog?.querySelector(':scope > header');
const footer = dialog?.querySelector(':scope > footer');
const body = dialog?.querySelector('.media-lightbox-body');
const meta = document.getElementById('media-lightbox-meta');
if (!modal || !dialog || !header || !footer || !body) return;
if (!header.querySelector('.ux-media-lightbox-nav')) {
const nav = createElement('div', 'ux-media-lightbox-nav');
const previous = createElement('button', 'icon-button', '');
previous.type = 'button';
previous.dataset.uxMediaPreviewNav = 'previous';
previous.setAttribute('aria-label', 'Vorheriges Bild');
previous.addEventListener('click', () => navigatePreview(-1));
const position = createElement('span', 'ux-media-lightbox-position', '0 Bilder');
position.id = 'ux-media-lightbox-position';
const next = createElement('button', 'icon-button', '');
next.type = 'button';
next.dataset.uxMediaPreviewNav = 'next';
next.setAttribute('aria-label', 'Nächstes Bild');
next.addEventListener('click', () => navigatePreview(1));
nav.append(previous, position, next);
header.insertBefore(nav, header.querySelector('.media-lightbox-close'));
}
if (meta && !meta.closest('.media-lightbox-meta-details')) {
const details = createElement('details', 'media-lightbox-meta-details');
details.open = true;
details.append(createElement('summary', '', 'Bildinformationen anzeigen'), meta);
body.append(details);
}
if (!document.getElementById('ux-media-lightbox-use')) {
const use = createElement('button', 'vd-button primary ux-media-use', 'Im Artikel verwenden');
use.type = 'button';
use.id = 'ux-media-lightbox-use';
use.addEventListener('click', () => activeCard()?.querySelector('[data-media-use]')?.click());
footer.prepend(use);
}
}
function bindControls() {
const section = root();
if (!section || section.dataset.uxMediaBound === 'true') return;
section.dataset.uxMediaBound = 'true';
for (const id of ['media-search', 'media-source', 'media-sort', 'media-favorites-only', 'media-page-size']) {
const control = document.getElementById(id);
control?.addEventListener(controlEvent(control), queueRefresh);
}
document.getElementById('media-view-grid')?.addEventListener('click', queueRefresh);
document.getElementById('media-view-list')?.addEventListener('click', queueRefresh);
section.addEventListener('click', event => {
const preview = event.target.closest?.('[data-media-preview]');
if (preview) {
activePath = preview.dataset.mediaPreview || '';
queueRefresh();
}
if (event.target.closest?.('[data-media-source], [data-media-select], [data-media-favorite]')) queueRefresh();
}, true);
document.addEventListener('keydown', event => {
const modal = document.getElementById('media-lightbox');
if (!modal || modal.classList.contains('hidden')) return;
if (event.target.closest('input, textarea, select')) return;
if (event.key === 'ArrowLeft') {
event.preventDefault();
navigatePreview(-1);
}
if (event.key === 'ArrowRight') {
event.preventDefault();
navigatePreview(1);
}
});
}
function observeDynamicContent() {
const grid = document.getElementById('media-grid');
const count = document.getElementById('media-selected-count');
if (grid && !gridObserver) {
gridObserver = new MutationObserver(queueRefresh);
gridObserver.observe(grid, { childList: true, subtree: true });
}
if (count && !countObserver) {
countObserver = new MutationObserver(queueRefresh);
countObserver.observe(count, { childList: true, characterData: true, subtree: true });
}
window.addEventListener('beforeunload', () => {
gridObserver?.disconnect();
countObserver?.disconnect();
if (refreshFrame) cancelAnimationFrame(refreshFrame);
}, { once: true });
}
function refreshPresentation() {
updateActiveFilters();
updateSelectionState();
annotateCards();
updateResultSummary();
updatePaginationState();
updateLightboxNavigation();
}
function initializeMediaUx() {
if (!root() || root().dataset.uxMediaReady === 'true') return;
root().dataset.uxMediaReady = 'true';
createCommandCenter();
reorganizeToolbar();
createActiveFilters();
enhanceBatchBar();
enhanceLightbox();
bindControls();
observeDynamicContent();
refreshPresentation();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeMediaUx, { once: true });
} else {
initializeMediaUx();
}
})();
+26
View File
@@ -0,0 +1,26 @@
#admin-modules-panel[data-ux-workspace-ready="1"],#extensions[data-ux-workspace-ready="1"]{--ux-workspace-gap:clamp(14px,2vw,24px)}
.ux-module-command,.ux-extension-board{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--ux-workspace-gap);align-items:center;padding:clamp(18px,2.6vw,30px);margin:0 0 16px;border:1px solid var(--vd-border,#d8dee8);border-radius:var(--vd-radius-xl,20px);background:linear-gradient(135deg,var(--vd-surface,#fff),var(--vd-surface-subtle,#f6f8fb));box-shadow:var(--vd-shadow-sm,0 8px 24px rgba(15,23,42,.06))}
.ux-module-command-copy h3,.ux-extension-board h3{margin:4px 0 6px;font-size:clamp(1.2rem,2vw,1.65rem)}
.ux-module-command-copy p,.ux-extension-board p{margin:0;max-width:72ch;color:var(--vd-text-muted,#667085)}
.ux-module-kicker{display:block;color:var(--vd-accent,#ff6b2c);font-size:.75rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase}
.ux-module-health{display:grid;grid-template-columns:repeat(4,minmax(74px,1fr));gap:8px;min-width:min(100%,390px)}
.ux-module-health-item{display:grid;gap:2px;padding:12px 14px;border:1px solid var(--vd-border,#d8dee8);border-radius:14px;background:var(--vd-surface,#fff);text-align:center}
.ux-module-health-item small{color:var(--vd-text-muted,#667085)}.ux-module-health-item strong{font-size:1.2rem}.ux-module-health-item[data-tone="danger"]{border-color:var(--vd-danger,#d92d20);background:color-mix(in srgb,var(--vd-danger,#d92d20) 8%,var(--vd-surface,#fff))}
.ux-module-filterbar{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 14px;padding:6px;border:1px solid var(--vd-border,#d8dee8);border-radius:16px;background:var(--vd-surface-subtle,#f6f8fb)}
.ux-module-filter{border:0;border-radius:11px;background:transparent;color:var(--vd-text-muted,#667085);padding:9px 13px;font-weight:700;cursor:pointer}.ux-module-filter[aria-pressed="true"]{background:var(--vd-surface,#fff);color:var(--vd-text,#101828);box-shadow:var(--vd-shadow-xs,0 2px 8px rgba(15,23,42,.08))}
.ux-module-tools{display:grid;grid-template-columns:minmax(220px,1fr) auto minmax(250px,auto);gap:10px;align-items:start;margin-bottom:18px}.ux-module-tools>input{min-height:42px}
.ux-module-install{border:1px solid var(--vd-border,#d8dee8);border-radius:14px;background:var(--vd-surface,#fff)}.ux-module-install summary{display:flex;justify-content:space-between;gap:16px;align-items:center;padding:11px 14px;cursor:pointer}.ux-module-install summary small{color:var(--vd-text-muted,#667085)}.ux-module-install-body{display:flex;gap:10px;align-items:center;padding:0 14px 14px}.ux-module-install-body input{min-width:0;max-width:320px}
#admin-modules-panel[data-ux-workspace-ready="1"] #module-manager-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
#admin-modules-panel[data-ux-workspace-ready="1"] .module-card{height:100%;outline:none;transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease}
#admin-modules-panel[data-ux-workspace-ready="1"] .module-card:hover{transform:translateY(-2px)}
#admin-modules-panel[data-ux-workspace-ready="1"] .module-card:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,var(--vd-focus,#2563eb) 30%,transparent);border-color:var(--vd-focus,#2563eb)}
#admin-modules-panel[data-ux-workspace-ready="1"] .module-card-actions{margin-top:auto;display:flex;gap:8px;flex-wrap:wrap}
#admin-modules-panel[data-ux-workspace-ready="1"] .module-error{order:-1}
.ux-extension-board{margin-top:16px}.ux-extension-status{display:flex;gap:10px;align-items:center;justify-content:flex-end;flex-wrap:wrap}.ux-extension-status #extensions-active-browser{margin:0}
#extensions[data-ux-workspace-ready="1"] #extensions-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
#extensions[data-ux-workspace-ready="1"] #extensions-grid>*{height:100%}
@media (max-width:1100px){.ux-module-command,.ux-extension-board{grid-template-columns:1fr}.ux-module-health{min-width:0}.ux-extension-status{justify-content:flex-start}.ux-module-tools{grid-template-columns:1fr auto}.ux-module-install{grid-column:1/-1}}
@media (max-width:780px){#admin-modules-panel[data-ux-workspace-ready="1"] #module-manager-list,#extensions[data-ux-workspace-ready="1"] #extensions-grid{grid-template-columns:1fr}.ux-module-health{grid-template-columns:repeat(2,1fr)}.ux-module-tools{grid-template-columns:1fr}.ux-module-install{grid-column:auto}.ux-module-install-body{align-items:stretch;flex-direction:column}.ux-module-filterbar{overflow-x:auto;flex-wrap:nowrap}.ux-module-filter{white-space:nowrap}}
@media (max-width:520px){.ux-module-command,.ux-extension-board{padding:16px;border-radius:16px}.ux-module-health{grid-template-columns:1fr 1fr}.ux-extension-status{align-items:stretch;flex-direction:column}.ux-extension-status>*{width:100%}}
@media (prefers-reduced-motion:reduce){#admin-modules-panel[data-ux-workspace-ready="1"] .module-card{transition:none}#admin-modules-panel[data-ux-workspace-ready="1"] .module-card:hover{transform:none}}
.ux-module-filter:focus-visible,.ux-module-install summary:focus-visible{outline:3px solid color-mix(in srgb,var(--vd-focus,#2563eb) 35%,transparent);outline-offset:2px}
+268
View File
@@ -0,0 +1,268 @@
(() => {
'use strict';
function loadCommandPlatformAssets() {
if (!document.querySelector('link[data-ux-command-platform]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/command-platform.css?v=1.44.0';
stylesheet.setAttribute('data-ux-command-platform', 'style');
document.head.append(stylesheet);
}
if (!document.querySelector('script[data-ux-command-platform]')) {
const script = document.createElement('script');
script.src = '/command-platform.js?v=1.44.0';
script.async = false;
script.setAttribute('data-ux-command-platform', 'script');
document.body.append(script);
}
}
function loadActionRegistryAssets() {
if (!document.querySelector('link[data-ux-action-registry]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/action-registry.css?v=1.44.0';
stylesheet.setAttribute('data-ux-action-registry', 'style');
document.head.append(stylesheet);
}
if (!document.querySelector('script[data-ux-action-registry]')) {
const script = document.createElement('script');
script.src = '/action-registry.js?v=1.44.0';
script.async = false;
script.setAttribute('data-ux-action-registry', 'script');
document.body.append(script);
}
}
function loadGlobalNavigationAssets() {
if (!document.querySelector('link[data-ux-global-navigation]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/global-navigation-ux.css?v=1.44.0';
stylesheet.setAttribute('data-ux-global-navigation', 'style');
document.head.append(stylesheet);
}
if (!document.querySelector('script[data-ux-global-navigation]')) {
const script = document.createElement('script');
script.src = '/global-navigation-ux.js?v=1.44.0';
script.async = false;
script.setAttribute('data-ux-global-navigation', 'script');
document.body.append(script);
}
}
function loadNavigationApi() {
if (document.querySelector('script[data-ux-navigation-api]')) return;
const script = document.createElement('script');
script.src = '/navigation-api.js?v=1.44.0';
script.async = false;
script.setAttribute('data-ux-navigation-api', 'script');
document.body.append(script);
}
function loadSafeActionAdapters() {
if (document.querySelector('script[data-ux-safe-action-adapters]')) return;
const script = document.createElement('script');
script.src = '/safe-action-adapters.js?v=1.44.0';
script.async = false;
script.setAttribute('data-ux-safe-action-adapters', 'script');
document.body.append(script);
}
loadCommandPlatformAssets();
loadActionRegistryAssets();
loadGlobalNavigationAssets();
loadNavigationApi();
loadSafeActionAdapters();
const moduleRoot = document.getElementById('admin-modules-panel');
const extensionRoot = document.getElementById('extensions');
if ((!moduleRoot && !extensionRoot) || document.documentElement.dataset.uxModuleExtensionReady === '1') return;
document.documentElement.dataset.uxModuleExtensionReady = '1';
const storageKey = 'vendoo_module_extension_workspace_v1';
const state = { moduleFilter: 'all', extensionFilter: 'all' };
try { Object.assign(state, JSON.parse(localStorage.getItem(storageKey) || '{}')); } catch {}
const persist = () => { try { localStorage.setItem(storageKey, JSON.stringify(state)); } catch {} };
const el = (tag, className, text) => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
};
function makeFilter(label, value, group, active) {
const button = el('button', 'ux-module-filter', label);
button.type = 'button';
button.dataset.uxModuleFilter = value;
button.dataset.uxModuleGroup = group;
button.setAttribute('aria-pressed', active ? 'true' : 'false');
return button;
}
function text(node) {
return String(node?.textContent || '').toLocaleLowerCase('de-DE');
}
function setupModules() {
if (!moduleRoot || moduleRoot.dataset.uxWorkspaceReady === '1') return;
moduleRoot.dataset.uxWorkspaceReady = '1';
const summary = moduleRoot.querySelector('#module-manager-summary');
const safety = moduleRoot.querySelector('#module-manager-safety');
const list = moduleRoot.querySelector('#module-manager-list');
const search = moduleRoot.querySelector('#module-manager-search');
const file = moduleRoot.querySelector('#module-manager-file');
const install = moduleRoot.querySelector('#module-manager-install');
const refreshButton = moduleRoot.querySelector('#module-manager-refresh');
if (!list) return;
const command = el('section', 'ux-module-command');
command.setAttribute('aria-label', 'Modulverwaltung');
const copy = el('div', 'ux-module-command-copy');
copy.append(el('span', 'ux-module-kicker', 'Systemerweiterungen'), el('h3', '', 'Module kontrolliert verwalten'), el('p', '', 'Abhängigkeiten, Vertrauensstatus und Betriebszustand zuerst prüfen. Änderungen bleiben bewusste Einzelaktionen.'));
const health = el('div', 'ux-module-health');
command.append(copy, health);
const filters = el('div', 'ux-module-filterbar');
[['Alle', 'all'], ['Aktiv', 'active'], ['Deaktiviert', 'disabled'], ['Installiert', 'external'], ['Probleme', 'problems']]
.forEach(([label, value]) => filters.append(makeFilter(label, value, 'modules', state.moduleFilter === value)));
const installDetails = el('details', 'ux-module-install');
const installSummary = el('summary');
installSummary.append(el('strong', '', 'Modulpaket installieren'), el('small', '', 'Nur geprüfte .vmod-Pakete'));
const installBody = el('div', 'ux-module-install-body');
if (file) installBody.append(file);
if (install) installBody.append(install);
installDetails.append(installSummary, installBody);
const tools = el('div', 'ux-module-tools');
if (search) tools.append(search);
if (refreshButton) tools.append(refreshButton);
tools.append(installDetails);
const anchor = summary || safety || list;
anchor.insertAdjacentElement('beforebegin', command);
command.insertAdjacentElement('afterend', filters);
filters.insertAdjacentElement('afterend', tools);
function classify(card) {
const value = text(card);
return {
active: value.includes('aktiv') && !value.includes('deaktiviert'),
disabled: value.includes('deaktiviert') || card.classList.contains('is-disabled'),
external: value.includes('installiert'),
problems: value.includes('fehler') || value.includes('ungültig') || value.includes('invalid') || card.classList.contains('is-invalid') || Boolean(card.querySelector('.module-error')),
};
}
function refresh() {
const cards = [...list.querySelectorAll('.module-card')];
const counts = { total: cards.length, active: 0, disabled: 0, external: 0, problems: 0 };
cards.forEach(card => {
const flags = classify(card);
Object.keys(flags).forEach(key => { if (flags[key]) counts[key] += 1; });
const visible = state.moduleFilter === 'all' || flags[state.moduleFilter];
card.hidden = !visible;
card.tabIndex = visible ? 0 : -1;
});
health.replaceChildren();
[['Gesamt', counts.total], ['Aktiv', counts.active], ['Installiert', counts.external], ['Probleme', counts.problems]]
.forEach(([label, value]) => {
const item = el('div', 'ux-module-health-item');
item.append(el('small', '', label), el('strong', '', String(value)));
if (label === 'Probleme' && value > 0) item.dataset.tone = 'danger';
health.append(item);
});
filters.querySelectorAll('[data-ux-module-filter]').forEach(button => button.setAttribute('aria-pressed', button.dataset.uxModuleFilter === state.moduleFilter ? 'true' : 'false'));
}
filters.addEventListener('click', event => {
const button = event.target.closest('[data-ux-module-filter]');
if (!button) return;
state.moduleFilter = button.dataset.uxModuleFilter;
persist();
refresh();
list.querySelector('.module-card:not([hidden])')?.focus({ preventScroll: true });
});
list.addEventListener('keydown', event => {
const card = event.target.closest('.module-card');
if (!card || !['Enter', ' '].includes(event.key)) return;
if (event.target.closest('button, a, input, select')) return;
event.preventDefault();
card.querySelector('.module-card-actions button:not(:disabled), .module-card-actions a')?.focus();
});
const observer = new MutationObserver(() => requestAnimationFrame(refresh));
observer.observe(list, { childList: true });
refresh();
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
function setupExtensions() {
if (!extensionRoot || extensionRoot.dataset.uxWorkspaceReady === '1') return;
extensionRoot.dataset.uxWorkspaceReady = '1';
const grid = extensionRoot.querySelector('#extensions-grid');
const activeBrowser = extensionRoot.querySelector('#extensions-active-browser');
const prepare = extensionRoot.querySelector('#extensions-prepare-active');
if (!grid) return;
const board = el('section', 'ux-extension-board');
const copy = el('div');
copy.append(el('span', 'ux-module-kicker', 'Browser-Verbindungen'), el('h3', '', 'Extensions vorbereiten und prüfen'), el('p', '', 'Vendoo bereitet Pakete und Pfade vor. Die endgültige Browserfreigabe bleibt immer manuell.'));
const status = el('div', 'ux-extension-status');
board.append(copy, status);
const filters = el('div', 'ux-module-filterbar ux-extension-filterbar');
[['Alle', 'all'], ['Bereit', 'ready'], ['Verbunden', 'connected'], ['Aktion nötig', 'attention']]
.forEach(([label, value]) => filters.append(makeFilter(label, value, 'extensions', state.extensionFilter === value)));
grid.insertAdjacentElement('beforebegin', filters);
filters.insertAdjacentElement('beforebegin', board);
if (activeBrowser) status.append(activeBrowser);
if (prepare) status.append(prepare);
function classify(card) {
const value = text(card);
return {
connected: value.includes('verbunden') || value.includes('aktiv'),
ready: value.includes('bereit') || value.includes('vorbereitet') || value.includes('installiert'),
attention: value.includes('nicht') || value.includes('fehler') || value.includes('aktion') || value.includes('installieren'),
};
}
function refresh() {
const cards = [...grid.children].filter(node => node.nodeType === 1);
let connected = 0;
let attention = 0;
cards.forEach(card => {
const flags = classify(card);
if (flags.connected) connected += 1;
if (flags.attention) attention += 1;
card.hidden = !(state.extensionFilter === 'all' || flags[state.extensionFilter]);
});
board.dataset.connected = String(connected);
board.dataset.attention = String(attention);
filters.querySelectorAll('[data-ux-module-filter]').forEach(button => button.setAttribute('aria-pressed', button.dataset.uxModuleFilter === state.extensionFilter ? 'true' : 'false'));
}
filters.addEventListener('click', event => {
const button = event.target.closest('[data-ux-module-filter]');
if (!button) return;
state.extensionFilter = button.dataset.uxModuleFilter;
persist();
refresh();
});
const observer = new MutationObserver(() => requestAnimationFrame(refresh));
observer.observe(grid, { childList: true, subtree: true, characterData: true });
refresh();
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
setupModules();
setupExtensions();
})();
+84
View File
@@ -0,0 +1,84 @@
(() => {
'use strict';
const root = document.documentElement;
if (root.dataset.uxNavigationApiReady === '1') return;
root.dataset.uxNavigationApiReady = '1';
let observer = null;
let lastView = '';
function activeView() {
return document.querySelector('.sidebar-nav .nav-item.active[data-tab]')?.dataset.tab
|| document.querySelector('.tab-content.active')?.id
|| 'dashboard';
}
function navItem(view) {
const normalized = String(view || '').trim();
if (!normalized) return null;
const items = document.querySelectorAll('.sidebar-nav .nav-item[data-tab]');
return [...items].find(item => item.dataset.tab === normalized) || null;
}
function canOpen(view) {
const item = navItem(view);
return Boolean(item
&& !item.hidden
&& !item.classList.contains('hidden')
&& item.getAttribute('aria-hidden') !== 'true'
&& item.getAttribute('aria-disabled') !== 'true');
}
function open(view) {
const item = navItem(view);
if (!canOpen(view) || typeof item?.click !== 'function') return false;
item.click();
return true;
}
function emitChange(source = 'observer') {
const currentView = activeView();
if (currentView === lastView) return;
const previousView = lastView || null;
lastView = currentView;
window.dispatchEvent(new CustomEvent('vendoo:navigation-changed', {
detail: { currentView, previousView, source },
}));
}
const api = {};
Object.defineProperties(api, {
currentView: {
enumerable: true,
get: activeView,
},
canOpen: {
enumerable: true,
value: canOpen,
},
open: {
enumerable: true,
value: open,
},
});
window.VendooNavigation = Object.freeze(api);
function observe() {
const nav = document.querySelector('.sidebar-nav');
if (!nav || observer) return;
lastView = activeView();
observer = new MutationObserver(() => emitChange('observer'));
observer.observe(nav, {
subtree: true,
attributes: true,
attributeFilter: ['class', 'hidden', 'aria-hidden', 'aria-disabled'],
});
}
observe();
window.addEventListener('beforeunload', () => observer?.disconnect(), { once: true });
window.dispatchEvent(new CustomEvent('vendoo:navigation-api-ready', {
detail: { currentView: activeView(), version: '1.44.0' },
}));
})();
+595
View File
@@ -0,0 +1,595 @@
/* Vendoo Publish Center UX 1.44.0
* Klare Trennung zwischen Vorbereitung, direktem Publishing und Betriebsansichten.
*/
#publish.ux-publish-ready {
--ux-publish-list-width: minmax(300px, .72fr);
}
.ux-publish-guide {
align-items: center;
background: linear-gradient(135deg, var(--vd-surface-raised), var(--vd-surface-muted));
border: 1px solid var(--vd-line);
border-radius: var(--vd-radius-lg);
display: grid;
gap: 22px;
grid-template-columns: minmax(240px, .9fr) minmax(460px, 1.3fr);
margin-bottom: 16px;
padding: 18px 20px;
}
.ux-publish-guide-eyebrow {
color: var(--vd-brand);
display: block;
font-size: 10px;
font-weight: 780;
letter-spacing: .08em;
margin-bottom: 5px;
text-transform: uppercase;
}
.ux-publish-guide-copy h3 {
color: var(--vd-ink);
font-size: clamp(18px, 2vw, 23px);
letter-spacing: -.025em;
line-height: 1.16;
margin: 0 0 6px;
}
.ux-publish-guide-copy p {
color: var(--vd-ink-secondary);
line-height: 1.5;
margin: 0;
}
.ux-publish-stages {
display: grid;
gap: 8px;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.ux-publish-stage {
align-items: center;
background: var(--vd-surface);
border: 1px solid var(--vd-line);
border-radius: var(--vd-radius-md);
display: grid;
gap: 9px;
grid-template-columns: 29px minmax(0, 1fr);
min-height: 58px;
padding: 9px 10px;
}
.ux-publish-stage-number {
align-items: center;
background: var(--vd-surface-muted);
border-radius: 50%;
color: var(--vd-ink-muted);
display: inline-flex;
font-size: 11px;
font-weight: 760;
height: 29px;
justify-content: center;
width: 29px;
}
.ux-publish-stage-copy strong,
.ux-publish-stage-copy small {
display: block;
}
.ux-publish-stage-copy strong {
color: var(--vd-ink);
font-size: 11px;
}
.ux-publish-stage-copy small {
color: var(--vd-ink-muted);
font-size: 8.5px;
line-height: 1.3;
margin-top: 2px;
}
.ux-publish-stage.is-active {
background: color-mix(in srgb, var(--vd-brand-soft) 46%, var(--vd-surface));
border-color: color-mix(in srgb, var(--vd-brand) 42%, var(--vd-line));
}
.ux-publish-stage.is-active .ux-publish-stage-number,
.ux-publish-stage.is-complete .ux-publish-stage-number {
background: var(--vd-brand);
color: white;
}
.ux-publish-stage.is-complete {
border-color: color-mix(in srgb, var(--vd-success) 28%, var(--vd-line));
}
#publish .publish-view-tabs {
background: transparent;
border: 0;
margin-bottom: 14px;
overflow: visible;
padding: 0;
}
.ux-publish-view-groups {
display: grid;
gap: 12px;
grid-template-columns: minmax(300px, .82fr) minmax(430px, 1.18fr);
}
.ux-publish-view-group {
background: var(--vd-surface-raised);
border: 1px solid var(--vd-line);
border-radius: var(--vd-radius-md);
min-width: 0;
padding: 10px;
}
.ux-publish-view-group-label {
color: var(--vd-ink-muted);
display: block;
font-size: 8.5px;
font-weight: 760;
letter-spacing: .08em;
margin: 0 3px 7px;
text-transform: uppercase;
}
.ux-publish-view-group-items {
display: grid;
gap: 7px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ux-publish-view-group[data-publish-view-group="betrieb"] .ux-publish-view-group-items {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
#publish .publish-view-tab {
align-items: center;
background: var(--vd-surface);
border: 1px solid transparent;
border-radius: var(--vd-radius-sm);
color: var(--vd-ink-secondary);
display: flex;
gap: 8px;
justify-content: space-between;
min-height: 51px;
padding: 8px 9px;
text-align: left;
white-space: normal;
}
#publish .publish-view-tab:hover {
border-color: var(--vd-line-strong);
color: var(--vd-ink);
}
#publish .publish-view-tab.active {
background: var(--vd-brand-soft);
border-color: color-mix(in srgb, var(--vd-brand) 42%, var(--vd-line));
color: var(--vd-brand);
box-shadow: none;
}
.ux-publish-tab-copy {
min-width: 0;
}
.ux-publish-tab-title,
.ux-publish-tab-help {
display: block;
}
.ux-publish-tab-title {
font-size: 10.5px;
line-height: 1.2;
}
.ux-publish-tab-help {
color: var(--vd-ink-muted);
font-size: 7.8px;
font-weight: 520;
line-height: 1.3;
margin-top: 3px;
}
#publish .publish-view-tab.active .ux-publish-tab-help {
color: color-mix(in srgb, var(--vd-brand) 72%, var(--vd-ink-muted));
}
#publish .publish-tab-count,
#publish .publish-tab-badge {
flex: 0 0 auto;
margin: 0;
}
.ux-publish-status-drawer {
background: var(--vd-surface-raised);
border: 1px solid var(--vd-line);
border-radius: var(--vd-radius-md);
margin-bottom: 14px;
}
.ux-publish-status-summary {
align-items: center;
cursor: pointer;
display: flex;
gap: 16px;
justify-content: space-between;
list-style: none;
min-height: 58px;
padding: 10px 14px;
}
.ux-publish-status-summary::-webkit-details-marker {
display: none;
}
.ux-publish-status-summary::after {
color: var(--vd-ink-muted);
content: '';
font-size: 19px;
order: 3;
transform: rotate(90deg);
transition: transform .15s ease;
}
.ux-publish-status-drawer:not([open]) .ux-publish-status-summary::after {
transform: rotate(0deg);
}
.ux-publish-status-summary-copy {
min-width: 0;
}
.ux-publish-status-summary-copy strong,
.ux-publish-status-summary-copy small {
display: block;
}
.ux-publish-status-summary-copy strong {
color: var(--vd-ink);
font-size: 11px;
}
.ux-publish-status-summary-copy small {
color: var(--vd-ink-muted);
font-size: 8.5px;
margin-top: 3px;
}
.ux-publish-health {
background: var(--vd-success-soft);
border: 1px solid color-mix(in srgb, var(--vd-success) 30%, var(--vd-line));
border-radius: 999px;
color: var(--vd-success);
flex: 0 0 auto;
font-size: 9px;
font-weight: 720;
padding: 5px 9px;
}
.ux-publish-health.is-active {
background: var(--vd-warning-soft);
border-color: color-mix(in srgb, var(--vd-warning) 34%, var(--vd-line));
color: var(--vd-warning);
}
.ux-publish-health.is-danger {
background: var(--vd-danger-soft);
border-color: color-mix(in srgb, var(--vd-danger) 34%, var(--vd-line));
color: var(--vd-danger);
}
.ux-publish-status-content {
border-top: 1px solid var(--vd-line);
padding: 12px;
}
.ux-publish-status-content .publish-status-rail {
display: grid;
gap: 12px;
grid-template-columns: repeat(3, minmax(0, 1fr));
width: 100%;
}
.ux-publish-status-content .publish-rail-section {
margin: 0;
min-width: 0;
}
#publish .publish-center-grid {
align-items: start;
display: grid;
gap: 14px;
grid-template-columns: var(--ux-publish-list-width) minmax(0, 1.5fr);
}
#publish[data-publish-operational] .publish-center-grid {
grid-template-columns: minmax(0, 1fr);
}
#publish[data-publish-operational] .publish-list-panel {
display: none;
}
#publish[data-publish-operational] .publish-workspace-panel {
min-width: 0;
width: 100%;
}
#publish .publish-list-panel,
#publish .publish-workspace-panel {
border-radius: var(--vd-radius-md);
}
#publish .publish-list-panel-head {
align-items: center;
gap: 10px;
}
#publish .publish-list-filters {
gap: 8px;
}
.ux-publish-active-filters {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 5px;
padding: 0 12px 10px;
}
.ux-publish-active-filters.is-empty {
display: none;
}
.ux-publish-active-filters > strong {
color: var(--vd-ink-muted);
font-size: 8px;
letter-spacing: .06em;
margin-right: 2px;
text-transform: uppercase;
}
.ux-publish-filter-chip {
align-items: center;
background: var(--vd-surface-muted);
border: 1px solid var(--vd-line);
border-radius: 999px;
color: var(--vd-ink-secondary);
display: inline-flex;
font-size: 8.5px;
gap: 4px;
min-height: 25px;
padding: 0 3px 0 8px;
}
.ux-publish-filter-clear {
align-items: center;
background: transparent;
border: 0;
border-radius: 50%;
color: var(--vd-ink-muted);
cursor: pointer;
display: inline-flex;
font-size: 14px;
height: 19px;
justify-content: center;
padding: 0;
width: 19px;
}
.ux-publish-filter-clear:hover {
background: var(--vd-surface-raised);
color: var(--vd-danger);
}
#publish .publish-batch-bar {
align-items: center;
background: var(--vd-surface-raised);
border-top: 1px solid var(--vd-line);
display: none;
gap: 7px;
padding: 9px 10px;
}
#publish[data-has-publish-selection] .publish-batch-bar {
display: flex;
}
.ux-publish-batch-primary {
align-items: center;
display: flex;
flex: 1 1 auto;
gap: 6px;
}
.ux-publish-batch-more {
position: relative;
}
.ux-publish-batch-more > summary {
align-items: center;
border: 1px solid var(--vd-line);
border-radius: 7px;
color: var(--vd-ink-secondary);
cursor: pointer;
display: inline-flex;
font-size: 9px;
font-weight: 650;
list-style: none;
min-height: 31px;
padding: 0 9px;
}
.ux-publish-batch-more > summary::-webkit-details-marker {
display: none;
}
.ux-publish-batch-more > summary::after {
content: '⌄';
margin-left: 6px;
}
.ux-publish-batch-secondary {
background: var(--vd-surface-raised);
border: 1px solid var(--vd-line);
border-radius: var(--vd-radius-sm);
box-shadow: var(--vd-shadow-float);
display: grid;
gap: 5px;
min-width: 185px;
padding: 7px;
position: absolute;
right: 0;
top: calc(100% + 7px);
z-index: 45;
}
.ux-publish-batch-secondary .vd-button {
justify-content: flex-start;
width: 100%;
}
#publish .publish-list-card {
border-color: transparent;
min-height: 86px;
}
#publish .publish-list-card:hover,
#publish .publish-list-card:focus-visible {
border-color: var(--vd-line-strong);
}
#publish .publish-list-card.is-active {
border-color: color-mix(in srgb, var(--vd-brand) 46%, var(--vd-line));
box-shadow: inset 3px 0 0 var(--vd-brand);
}
#publish .publish-list-card:focus-visible,
#publish .publish-view-tab:focus-visible,
.ux-publish-status-summary:focus-visible,
.ux-publish-filter-clear:focus-visible,
.ux-publish-batch-more > summary:focus-visible {
outline: 3px solid var(--vd-focus);
outline-offset: 2px;
}
#publish[data-publish-mode="smart"] .publish-workspace-panel,
#publish[data-publish-mode="direct"] .publish-workspace-panel {
min-height: 520px;
}
#publish[data-publish-operational] .publish-operation-header,
#publish[data-publish-operational] .publishing-hardened-header {
position: sticky;
top: calc(var(--vd-topbar-height) + 8px);
z-index: 10;
}
#publish[data-publish-operational] .publishing-toolbar {
grid-template-columns: minmax(220px, 1fr) repeat(3, minmax(130px, auto));
}
#publish .publish-heading-actions #publish-add-selected-ebay[hidden] {
display: none;
}
@media (max-width: 1180px) {
.ux-publish-guide,
.ux-publish-view-groups {
grid-template-columns: 1fr;
}
#publish .publish-center-grid {
grid-template-columns: minmax(270px, .7fr) minmax(0, 1.3fr);
}
.ux-publish-status-content .publish-status-rail {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ux-publish-status-content .publish-activity-section {
grid-column: 1 / -1;
}
}
@media (max-width: 900px) {
.ux-publish-stages {
grid-template-columns: 1fr;
}
#publish .publish-center-grid {
grid-template-columns: 1fr;
}
#publish:not([data-publish-operational]) .publish-list-panel {
max-height: 520px;
}
.ux-publish-status-content .publish-status-rail {
grid-template-columns: 1fr;
}
.ux-publish-status-content .publish-activity-section {
grid-column: auto;
}
#publish[data-publish-operational] .publishing-toolbar {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 680px) {
.ux-publish-guide {
padding: 15px;
}
.ux-publish-view-group-items,
.ux-publish-view-group[data-publish-view-group="betrieb"] .ux-publish-view-group-items {
grid-template-columns: 1fr;
}
.ux-publish-status-summary {
align-items: flex-start;
flex-wrap: wrap;
}
.ux-publish-health {
margin-left: 0;
}
#publish .publish-batch-bar,
.ux-publish-batch-primary {
align-items: stretch;
flex-direction: column;
width: 100%;
}
.ux-publish-batch-primary .vd-button,
.ux-publish-batch-more,
.ux-publish-batch-more > summary {
justify-content: center;
width: 100%;
}
.ux-publish-batch-secondary {
left: 0;
right: auto;
width: 100%;
}
#publish[data-publish-operational] .publishing-toolbar {
grid-template-columns: 1fr;
}
}
@media (prefers-reduced-motion: reduce) {
.ux-publish-status-summary::after {
transition: none;
}
}
+370
View File
@@ -0,0 +1,370 @@
(() => {
'use strict';
const STATUS_DRAWER_KEY = 'vendoo_publish_status_open_v1';
const OPERATIONAL_VIEWS = new Set(['queue', 'errors', 'scheduled']);
const VIEW_META = {
smart: {
title: 'Manuell veröffentlichen',
help: 'Daten und Fotos kontrolliert kopieren',
stage: 2,
context: 'Artikel auswählen und Smart Copy öffnen.',
},
direct: {
title: 'Direkt veröffentlichen',
help: 'Über verbundene Marktplatzkonten',
stage: 2,
context: 'Artikel prüfen, speichern und sicher einreihen.',
},
queue: {
title: 'Aufträge',
help: 'Publishing-Queue überwachen',
stage: 3,
context: 'Laufende und abgeschlossene Publishing-Aufträge verwalten.',
},
scheduled: {
title: 'Geplant',
help: 'Zeitgesteuerte Veröffentlichungen',
stage: 3,
context: 'Geplante Veröffentlichungen kontrollieren oder entfernen.',
},
errors: {
title: 'Fehler & Verlauf',
help: 'Probleme und Ereignisse prüfen',
stage: 3,
context: 'Fehler nachvollziehen und betroffene Aufträge öffnen.',
},
};
let refreshQueued = false;
let publishObserver = null;
function publishSection() {
return document.getElementById('publish');
}
function createElement(tag, className, text) {
const element = document.createElement(tag);
if (className) element.className = className;
if (text !== undefined) element.textContent = text;
return element;
}
function setText(node, value) {
if (node && node.textContent !== String(value)) node.textContent = String(value);
}
function activeView() {
return publishSection()?.querySelector('[data-publish-view].active')?.dataset.publishView || 'smart';
}
function decorateViewButton(button) {
if (!button || button.dataset.uxPublishDecorated === 'true') return;
const view = button.dataset.publishView;
const meta = VIEW_META[view];
if (!meta) return;
const badge = button.querySelector('.publish-tab-count, .publish-tab-badge');
const title = createElement('strong', 'ux-publish-tab-title', meta.title);
const help = createElement('small', 'ux-publish-tab-help', meta.help);
const copy = createElement('span', 'ux-publish-tab-copy');
copy.append(title, help);
button.replaceChildren(copy);
if (badge) button.append(badge);
button.setAttribute('role', 'tab');
button.setAttribute('aria-label', `${meta.title}: ${meta.help}`);
button.dataset.uxPublishDecorated = 'true';
}
function createViewGroup(label, views) {
const group = createElement('section', 'ux-publish-view-group');
group.dataset.publishViewGroup = label.toLowerCase();
const heading = createElement('span', 'ux-publish-view-group-label', label);
const items = createElement('div', 'ux-publish-view-group-items');
for (const view of views) {
const button = publishSection()?.querySelector(`[data-publish-view="${view}"]`);
if (!button) continue;
decorateViewButton(button);
items.append(button);
}
group.append(heading, items);
return group;
}
function reorganizeViewNavigation() {
const nav = publishSection()?.querySelector('.publish-view-tabs');
if (!nav || nav.querySelector('.ux-publish-view-groups')) return;
nav.setAttribute('role', 'tablist');
const shell = createElement('div', 'ux-publish-view-groups');
shell.append(
createViewGroup('Vorbereiten', ['smart', 'direct']),
createViewGroup('Betrieb', ['queue', 'scheduled', 'errors']),
);
nav.replaceChildren(shell);
nav.addEventListener('keydown', event => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
const buttons = [...nav.querySelectorAll('[data-publish-view]')];
if (!buttons.length) return;
const current = Math.max(0, buttons.indexOf(document.activeElement));
let target = current;
if (event.key === 'ArrowRight') target = (current + 1) % buttons.length;
if (event.key === 'ArrowLeft') target = (current - 1 + buttons.length) % buttons.length;
if (event.key === 'Home') target = 0;
if (event.key === 'End') target = buttons.length - 1;
event.preventDefault();
buttons[target].focus();
buttons[target].click();
});
}
function createGuidance() {
const section = publishSection();
const nav = section?.querySelector('.publish-view-tabs');
if (!section || !nav || section.querySelector('.ux-publish-guide')) return;
const guide = createElement('section', 'ux-publish-guide');
guide.setAttribute('aria-label', 'Publishing-Ablauf');
const copy = createElement('div', 'ux-publish-guide-copy');
const eyebrow = createElement('span', 'ux-publish-guide-eyebrow', 'Sicher veröffentlichen');
const title = createElement('h3', '', 'Vom fertigen Artikel zum kontrollierten Auftrag');
const context = createElement('p', '', VIEW_META.smart.context);
context.id = 'ux-publish-context';
copy.append(eyebrow, title, context);
const stages = createElement('div', 'ux-publish-stages');
const stageData = [
['1', 'Artikel wählen', 'Suche und Filter nutzen'],
['2', 'Methode wählen', 'Smart Copy oder Direkt'],
['3', 'Auftrag überwachen', 'Queue, Planung und Fehler'],
];
for (const [number, label, help] of stageData) {
const stage = createElement('div', 'ux-publish-stage');
stage.dataset.publishStage = number;
const marker = createElement('span', 'ux-publish-stage-number', number);
const stageCopy = createElement('span', 'ux-publish-stage-copy');
stageCopy.append(createElement('strong', '', label), createElement('small', '', help));
stage.append(marker, stageCopy);
stages.append(stage);
}
guide.append(copy, stages);
nav.before(guide);
}
function moveStatusRail() {
const section = publishSection();
const grid = section?.querySelector('.publish-center-grid');
const rail = grid?.querySelector('.publish-status-rail');
if (!section || !grid || !rail || section.querySelector('.ux-publish-status-drawer')) return;
const details = createElement('details', 'ux-publish-status-drawer');
details.open = localStorage.getItem(STATUS_DRAWER_KEY) === 'true';
const summary = createElement('summary', 'ux-publish-status-summary');
const summaryCopy = createElement('span', 'ux-publish-status-summary-copy');
summaryCopy.append(
createElement('strong', '', 'Status, Schnellaktionen und Aktivität'),
createElement('small', '', 'Nur öffnen, wenn du den Publishing-Betrieb prüfen möchtest.'),
);
const health = createElement('span', 'ux-publish-health', 'Keine offenen Probleme');
health.id = 'ux-publish-health';
summary.append(summaryCopy, health);
const content = createElement('div', 'ux-publish-status-content');
content.append(rail);
details.append(summary, content);
details.addEventListener('toggle', () => localStorage.setItem(STATUS_DRAWER_KEY, String(details.open)));
grid.before(details);
}
function enhanceBatchActions() {
const bar = publishSection()?.querySelector('.publish-batch-bar');
if (!bar || bar.querySelector('.ux-publish-batch-primary')) return;
const primary = createElement('div', 'ux-publish-batch-primary');
const more = createElement('details', 'ux-publish-batch-more');
const summary = createElement('summary', '', 'Weitere Aktionen');
const secondary = createElement('div', 'ux-publish-batch-secondary');
for (const id of ['publish-batch-copy', 'publish-batch-ebay', 'publish-batch-schedule']) {
const button = document.getElementById(id);
if (button) primary.append(button);
}
for (const id of ['publish-download-photos', 'publish-batch-etsy']) {
const button = document.getElementById(id);
if (button) secondary.append(button);
}
more.append(summary, secondary);
bar.append(primary, more);
}
function createActiveFilters() {
const filters = publishSection()?.querySelector('.publish-list-filters');
if (!filters || document.getElementById('ux-publish-active-filters')) return;
const active = createElement('div', 'ux-publish-active-filters');
active.id = 'ux-publish-active-filters';
active.setAttribute('aria-live', 'polite');
filters.after(active);
}
function dispatchControl(control, value) {
if (!control) return;
control.value = value;
control.dispatchEvent(new Event(control.tagName === 'INPUT' ? 'input' : 'change', { bubbles: true }));
queueRefresh();
}
function createFilterChip(label, control, value = '') {
const chip = createElement('span', 'ux-publish-filter-chip');
chip.append(createElement('span', '', label));
const clear = createElement('button', 'ux-publish-filter-clear', '×');
clear.type = 'button';
clear.setAttribute('aria-label', `${label} entfernen`);
clear.addEventListener('click', () => dispatchControl(control, value));
chip.append(clear);
return chip;
}
function selectedOptionText(select) {
if (!select?.value) return '';
return select.options[select.selectedIndex]?.textContent?.trim() || select.value;
}
function updateActiveFilters() {
const container = document.getElementById('ux-publish-active-filters');
if (!container) return;
const search = document.getElementById('publish-search');
const platform = document.getElementById('publish-filter-platform');
const status = document.getElementById('publish-filter-status');
const signature = JSON.stringify([search?.value.trim() || '', platform?.value || '', status?.value || '']);
if (container.dataset.filterSignature === signature) return;
container.dataset.filterSignature = signature;
const chips = [];
if (search?.value.trim()) chips.push(createFilterChip(`Suche: ${search.value.trim()}`, search));
if (platform?.value) chips.push(createFilterChip(`Plattform: ${selectedOptionText(platform)}`, platform));
if (status?.value) chips.push(createFilterChip(`Status: ${selectedOptionText(status)}`, status));
container.replaceChildren();
container.classList.toggle('is-empty', chips.length === 0);
if (!chips.length) return;
container.append(createElement('strong', '', 'Aktive Filter'), ...chips);
}
function annotateListingCards() {
document.querySelectorAll('#publish-list .publish-list-card').forEach(card => {
card.tabIndex = 0;
const title = card.querySelector('.publish-card-copy > strong')?.textContent?.trim() || 'Artikel';
card.setAttribute('aria-label', `${title} für die Veröffentlichung auswählen`);
const status = card.querySelector('.publish-card-status');
if (status) card.dataset.publishStatus = [...status.classList].find(name => name.startsWith('status-'))?.slice(7) || 'prepared';
if (card.dataset.uxPublishKeyboard === 'true') return;
card.dataset.uxPublishKeyboard = 'true';
card.addEventListener('keydown', event => {
if ((event.key === 'Enter' || event.key === ' ') && event.target === card) {
event.preventDefault();
card.click();
}
});
});
}
function selectionCount() {
const text = document.getElementById('publish-selected-count')?.textContent || '0';
return Number.parseInt(text, 10) || 0;
}
function updateSelectionState() {
const section = publishSection();
const count = selectionCount();
if (!section) return;
section.toggleAttribute('data-has-publish-selection', count > 0);
const more = section.querySelector('.ux-publish-batch-more');
if (!count && more) more.open = false;
const headingAction = document.getElementById('publish-add-selected-ebay');
const shouldHide = count === 0;
if (headingAction && headingAction.hidden !== shouldHide) headingAction.hidden = shouldHide;
}
function updateHealth() {
const failed = Number(document.getElementById('publish-status-failed')?.textContent || 0);
const pending = Number(document.getElementById('publish-status-pending')?.textContent || 0);
const processing = Number(document.getElementById('publish-status-processing')?.textContent || 0);
const health = document.getElementById('ux-publish-health');
if (!health) return;
const className = `ux-publish-health${failed ? ' is-danger' : pending || processing ? ' is-active' : ''}`;
if (health.className !== className) health.className = className;
setText(health, failed ? `${failed} Fehler prüfen` : processing ? `${processing} in Bearbeitung` : pending ? `${pending} wartend` : 'Keine offenen Probleme');
}
function updateViewState() {
const section = publishSection();
if (!section) return;
const view = activeView();
const meta = VIEW_META[view] || VIEW_META.smart;
section.dataset.publishMode = view;
section.toggleAttribute('data-publish-operational', OPERATIONAL_VIEWS.has(view));
section.querySelectorAll('[data-publish-view]').forEach(button => {
const active = button.dataset.publishView === view;
button.setAttribute('aria-selected', String(active));
button.tabIndex = active ? 0 : -1;
});
section.querySelectorAll('[data-publish-stage]').forEach(stage => {
const number = Number(stage.dataset.publishStage);
stage.classList.toggle('is-active', number === meta.stage);
stage.classList.toggle('is-complete', number < meta.stage);
});
setText(document.getElementById('ux-publish-context'), meta.context);
}
function refreshPresentation() {
refreshQueued = false;
updateViewState();
updateSelectionState();
updateActiveFilters();
updateHealth();
annotateListingCards();
}
function queueRefresh() {
if (refreshQueued) return;
refreshQueued = true;
window.requestAnimationFrame(refreshPresentation);
}
function bindControls() {
for (const id of ['publish-search', 'publish-filter-platform', 'publish-filter-status']) {
const control = document.getElementById(id);
if (!control || control.dataset.uxPublishBound === 'true') continue;
control.dataset.uxPublishBound = 'true';
control.addEventListener('input', queueRefresh);
control.addEventListener('change', queueRefresh);
}
}
function observePublishCenter() {
const section = publishSection();
if (!section) return;
publishObserver = new MutationObserver(queueRefresh);
publishObserver.observe(section, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: ['class', 'disabled', 'hidden'],
});
window.addEventListener('beforeunload', () => publishObserver?.disconnect(), { once: true });
}
function initializePublishCenterUx() {
const section = publishSection();
if (!section || section.classList.contains('ux-publish-ready')) return;
reorganizeViewNavigation();
createGuidance();
moveStatusRail();
enhanceBatchActions();
createActiveFilters();
bindControls();
observePublishCenter();
refreshPresentation();
section.classList.add('ux-publish-ready');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePublishCenterUx, { once: true });
} else {
initializePublishCenterUx();
}
})();
+438
View File
@@ -0,0 +1,438 @@
#quality-center.ux-quality-center-ready {
--ux-quality-danger: var(--vd-danger, #b42318);
--ux-quality-warning: var(--vd-warning, #b54708);
--ux-quality-success: var(--vd-success, #067647);
--ux-quality-info: var(--vd-accent, #d94f2b);
}
#quality-center .ux-quality-priority-board {
display: grid;
gap: 18px;
margin: 0 0 20px;
padding: clamp(18px, 2vw, 26px);
border: 1px solid var(--vd-border, rgba(31, 35, 40, .12));
border-radius: var(--vd-radius-xl, 22px);
background: linear-gradient(135deg, color-mix(in srgb, var(--vd-surface, #fff) 94%, var(--vd-accent, #d94f2b) 6%), var(--vd-surface, #fff));
box-shadow: var(--vd-shadow-sm, 0 8px 24px rgba(18, 24, 40, .06));
}
#quality-center .ux-quality-priority-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
}
#quality-center .ux-quality-priority-head h3 {
margin: 4px 0 6px;
font-size: clamp(1.2rem, 2vw, 1.55rem);
}
#quality-center .ux-quality-priority-head p {
max-width: 760px;
margin: 0;
color: var(--vd-text-muted, #667085);
line-height: 1.55;
}
#quality-center .ux-quality-kicker {
color: var(--vd-accent, #d94f2b);
font-size: .76rem;
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
}
#quality-center .ux-quality-next {
flex: 0 0 auto;
}
#quality-center .ux-quality-priorities {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
#quality-center .ux-quality-priority {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 12px;
min-width: 0;
padding: 14px;
border: 1px solid var(--vd-border, rgba(31, 35, 40, .12));
border-radius: var(--vd-radius-lg, 16px);
background: var(--vd-surface, #fff);
color: inherit;
text-align: left;
cursor: pointer;
transition: border-color .16s ease, transform .16s ease, box-shadow .16s ease;
}
#quality-center .ux-quality-priority:hover {
transform: translateY(-1px);
border-color: color-mix(in srgb, var(--vd-accent, #d94f2b) 45%, var(--vd-border, #ddd));
}
#quality-center .ux-quality-priority.is-active {
border-color: var(--vd-accent, #d94f2b);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--vd-accent, #d94f2b) 14%, transparent);
}
#quality-center .ux-quality-priority-count {
display: grid;
place-items: center;
min-width: 44px;
min-height: 44px;
border-radius: 14px;
background: var(--vd-surface-subtle, #f6f7f8);
font-size: 1.25rem;
}
#quality-center [data-ux-quality-status="blocked"] .ux-quality-priority-count {
color: var(--ux-quality-danger);
background: color-mix(in srgb, var(--ux-quality-danger) 10%, transparent);
}
#quality-center [data-ux-quality-status="review"] .ux-quality-priority-count {
color: var(--ux-quality-warning);
background: color-mix(in srgb, var(--ux-quality-warning) 11%, transparent);
}
#quality-center [data-ux-quality-status="ready"] .ux-quality-priority-count {
color: var(--ux-quality-success);
background: color-mix(in srgb, var(--ux-quality-success) 10%, transparent);
}
#quality-center .ux-quality-priority-copy {
display: grid;
min-width: 0;
gap: 3px;
}
#quality-center .ux-quality-priority-copy b {
font-size: .95rem;
}
#quality-center .ux-quality-priority-copy small {
color: var(--vd-text-muted, #667085);
line-height: 1.35;
}
#quality-center .ux-quality-summary-secondary {
margin-bottom: 14px;
opacity: .88;
}
#quality-center .ux-quality-toolbar-shell {
display: grid;
gap: 12px;
}
#quality-center .ux-quality-toolbar-primary,
#quality-center .ux-quality-toolbar-primary .quality-search {
width: 100%;
}
#quality-center .ux-quality-filter-panel {
border-top: 1px solid var(--vd-border, rgba(31, 35, 40, .1));
padding-top: 10px;
}
#quality-center .ux-quality-filter-panel > summary,
#quality-center .ux-quality-secondary > summary,
#quality-center .ux-quality-more-actions > summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
cursor: pointer;
font-weight: 750;
list-style: none;
}
#quality-center .ux-quality-filter-panel > summary::-webkit-details-marker,
#quality-center .ux-quality-secondary > summary::-webkit-details-marker,
#quality-center .ux-quality-more-actions > summary::-webkit-details-marker {
display: none;
}
#quality-center .ux-quality-filter-panel > summary::after,
#quality-center .ux-quality-secondary > summary::after,
#quality-center .ux-quality-more-actions > summary::after {
content: "+";
font-size: 1.1rem;
color: var(--vd-text-muted, #667085);
}
#quality-center .ux-quality-filter-panel[open] > summary::after,
#quality-center .ux-quality-secondary[open] > summary::after,
#quality-center .ux-quality-more-actions[open] > summary::after {
content: "";
}
#quality-center .ux-quality-filter-body {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
padding-top: 12px;
}
#quality-center .ux-quality-filter-chips {
display: flex;
flex-wrap: wrap;
gap: 7px;
}
#quality-center .ux-quality-filter-chips.is-empty {
display: none;
}
#quality-center .ux-quality-filter-chip {
padding: 7px 10px;
border: 1px solid color-mix(in srgb, var(--vd-accent, #d94f2b) 28%, var(--vd-border, #ddd));
border-radius: 999px;
background: color-mix(in srgb, var(--vd-accent, #d94f2b) 7%, var(--vd-surface, #fff));
color: inherit;
font-size: .78rem;
cursor: pointer;
}
#quality-center .quality-workspace {
align-items: start;
grid-template-columns: minmax(300px, .85fr) minmax(0, 1.5fr);
}
#quality-center .quality-list-panel {
position: sticky;
top: 84px;
max-height: calc(100vh - 110px);
overflow: auto;
}
#quality-center .quality-list-item {
grid-template-columns: auto minmax(0, 1fr) auto;
}
#quality-center .quality-list-state {
grid-column: 2 / 4;
justify-self: start;
}
#quality-center .quality-detail-panel {
min-width: 0;
}
#quality-center .quality-detail-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
#quality-center .ux-quality-main-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
#quality-center .ux-quality-more-actions {
position: relative;
}
#quality-center .ux-quality-more-actions > summary {
min-height: 38px;
padding: 0 11px;
border: 1px solid var(--vd-border, rgba(31, 35, 40, .13));
border-radius: var(--vd-radius-md, 12px);
background: var(--vd-surface, #fff);
font-size: .84rem;
}
#quality-center .ux-quality-more-actions-body {
position: absolute;
z-index: 10;
top: calc(100% + 6px);
right: 0;
min-width: 190px;
padding: 8px;
border: 1px solid var(--vd-border, rgba(31, 35, 40, .13));
border-radius: var(--vd-radius-md, 12px);
background: var(--vd-surface, #fff);
box-shadow: var(--vd-shadow-lg, 0 18px 45px rgba(18, 24, 40, .16));
}
#quality-center .ux-quality-more-actions-body button {
width: 100%;
}
#quality-center .ux-quality-issue-section {
display: grid;
gap: 12px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--vd-border, rgba(31, 35, 40, .1));
}
#quality-center .ux-quality-section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-weight: 800;
}
#quality-center .ux-quality-issue-summary {
padding: 5px 9px;
border-radius: 999px;
background: var(--vd-surface-subtle, #f6f7f8);
color: var(--vd-text-muted, #667085);
font-size: .76rem;
}
#quality-center .ux-quality-issue-summary[data-tone="danger"] {
color: var(--ux-quality-danger);
background: color-mix(in srgb, var(--ux-quality-danger) 10%, transparent);
}
#quality-center .ux-quality-issue-summary[data-tone="warning"] {
color: var(--ux-quality-warning);
background: color-mix(in srgb, var(--ux-quality-warning) 10%, transparent);
}
#quality-center .ux-quality-issue-summary[data-tone="success"] {
color: var(--ux-quality-success);
background: color-mix(in srgb, var(--ux-quality-success) 10%, transparent);
}
#quality-center .quality-issue-tabs {
display: flex;
gap: 7px;
overflow-x: auto;
padding-bottom: 2px;
}
#quality-center .quality-issue-tabs button {
flex: 0 0 auto;
}
#quality-center .quality-issue-tabs button.ux-quality-has-priority {
border-color: color-mix(in srgb, var(--ux-quality-danger) 45%, var(--vd-border, #ddd));
}
#quality-center .quality-issues {
display: grid;
gap: 8px;
}
#quality-center .quality-issue {
grid-template-columns: auto minmax(0, 1fr) auto;
padding: 13px;
}
#quality-center .ux-quality-secondary {
margin-top: 18px;
padding: 14px 0 0;
border-top: 1px solid var(--vd-border, rgba(31, 35, 40, .1));
}
#quality-center .ux-quality-secondary > summary span:first-child {
display: grid;
gap: 2px;
}
#quality-center .ux-quality-secondary > summary small {
color: var(--vd-text-muted, #667085);
font-weight: 500;
}
#quality-center .ux-quality-secondary-badge {
margin-left: auto;
padding: 4px 8px;
border-radius: 999px;
background: var(--vd-surface-subtle, #f6f7f8);
color: var(--vd-text-muted, #667085);
font-size: .72rem;
}
#quality-center .ux-quality-secondary-body {
display: grid;
gap: 18px;
padding-top: 16px;
}
#quality-center .ux-quality-secondary-body > section,
#quality-center .ux-quality-secondary-body > div {
margin: 0;
}
#quality-center :is(button, input, select, summary):focus-visible {
outline: 3px solid color-mix(in srgb, var(--vd-accent, #d94f2b) 38%, transparent);
outline-offset: 2px;
}
@media (max-width: 1080px) {
#quality-center .ux-quality-priorities {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#quality-center .quality-workspace {
grid-template-columns: 1fr;
}
#quality-center .quality-list-panel {
position: static;
max-height: none;
}
}
@media (max-width: 760px) {
#quality-center .ux-quality-priority-head,
#quality-center .quality-detail-actions {
align-items: stretch;
flex-direction: column;
}
#quality-center .ux-quality-next,
#quality-center .ux-quality-main-actions,
#quality-center .ux-quality-main-actions > *,
#quality-center .ux-quality-more-actions,
#quality-center .ux-quality-more-actions > summary {
width: 100%;
}
#quality-center .ux-quality-filter-body {
grid-template-columns: 1fr;
}
#quality-center .ux-quality-more-actions-body {
position: static;
margin-top: 8px;
}
}
@media (max-width: 470px) {
#quality-center .ux-quality-priorities {
grid-template-columns: 1fr;
}
#quality-center .quality-summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#quality-center .quality-detail-head {
align-items: flex-start;
}
}
@media (prefers-reduced-motion: reduce) {
#quality-center *,
#quality-center *::before,
#quality-center *::after {
scroll-behavior: auto !important;
transition-duration: .01ms !important;
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
}
}
+349
View File
@@ -0,0 +1,349 @@
(() => {
'use strict';
const ROOT_ID = 'quality-center';
const FILTER_OPEN_KEY = 'vendoo_quality_filters_open';
const createNode = (tag, className = '', text = '') => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text) node.textContent = text;
return node;
};
const safeNumber = value => {
const match = String(value ?? '').replace(/\./g, '').match(/-?\d+/);
return match ? Number(match[0]) : 0;
};
const focusNode = node => {
if (!node) return;
node.scrollIntoView({ behavior: 'smooth', block: 'center' });
window.requestAnimationFrame(() => {
const target = node.matches('button,input,select,summary') ? node : node.querySelector('button,input,select,summary');
target?.focus({ preventScroll: true });
});
};
function createPriorityButton(status, label, copy) {
const button = createNode('button', 'ux-quality-priority');
button.type = 'button';
button.dataset.uxQualityStatus = status;
button.setAttribute('aria-pressed', 'false');
const count = createNode('strong', 'ux-quality-priority-count', '0');
count.dataset.uxQualityCount = status;
const body = createNode('span', 'ux-quality-priority-copy');
body.append(createNode('b', '', label), createNode('small', '', copy));
button.append(count, body);
return button;
}
function createPriorityBoard(root) {
if (root.querySelector('.ux-quality-priority-board')) return;
const summary = root.querySelector('.quality-summary-grid');
if (!summary) return;
const board = createNode('section', 'ux-quality-priority-board');
board.setAttribute('aria-label', 'Qualitätsprioritäten');
const heading = createNode('div', 'ux-quality-priority-head');
const copy = createNode('div');
copy.append(
createNode('span', 'ux-quality-kicker', 'Prioritäten statt Zahlenflut'),
createNode('h3', '', 'Was braucht jetzt Aufmerksamkeit?'),
createNode('p', '', 'Blocker zuerst beheben, danach Warnungen prüfen. Veröffentlichungsbereite Artikel bleiben erreichbar, stehen aber nicht im Weg.')
);
const next = createNode('button', 'vd-button primary ux-quality-next', 'Blockierte Artikel öffnen');
next.type = 'button';
next.dataset.uxQualityNext = 'blocked';
heading.append(copy, next);
const priorities = createNode('div', 'ux-quality-priorities');
priorities.setAttribute('role', 'group');
priorities.setAttribute('aria-label', 'Qualitätsstatus filtern');
priorities.append(
createPriorityButton('blocked', 'Blockiert', 'Harte Fehler verhindern die Veröffentlichung'),
createPriorityButton('review', 'Prüfen', 'Warnungen und Verbesserungen kontrollieren'),
createPriorityButton('ready', 'Bereit', 'Ohne Blocker veröffentlichbar'),
createPriorityButton('all', 'Alle Artikel', 'Gesamten Bestand anzeigen')
);
board.append(heading, priorities);
summary.before(board);
summary.classList.add('ux-quality-summary-secondary');
}
function restructureToolbar(root) {
const toolbar = root.querySelector('.quality-toolbar');
if (!toolbar || toolbar.dataset.uxQualityStructured === 'true') return;
const search = toolbar.querySelector('.quality-search');
const status = root.querySelector('#quality-status')?.closest('label');
const grade = root.querySelector('#quality-grade')?.closest('label');
const pageSize = root.querySelector('#quality-page-size')?.closest('label');
const shell = createNode('div', 'ux-quality-toolbar-shell');
const primary = createNode('div', 'ux-quality-toolbar-primary');
if (search) primary.append(search);
const filters = createNode('details', 'ux-quality-filter-panel');
const summary = createNode('summary', '', 'Filter & Anzeige');
const body = createNode('div', 'ux-quality-filter-body');
[status, grade, pageSize].filter(Boolean).forEach(node => body.append(node));
filters.append(summary, body);
try { filters.open = localStorage.getItem(FILTER_OPEN_KEY) === 'true'; } catch {}
filters.addEventListener('toggle', () => {
try { localStorage.setItem(FILTER_OPEN_KEY, String(filters.open)); } catch {}
});
const chips = createNode('div', 'ux-quality-filter-chips');
chips.setAttribute('aria-live', 'polite');
shell.append(primary, filters, chips);
toolbar.replaceChildren(shell);
toolbar.dataset.uxQualityStructured = 'true';
}
function createChip(label, onRemove) {
const button = createNode('button', 'ux-quality-filter-chip', `${label} ×`);
button.type = 'button';
button.addEventListener('click', onRemove);
return button;
}
function updateFilterChips(root) {
const chips = root.querySelector('.ux-quality-filter-chips');
if (!chips) return;
chips.replaceChildren();
const search = root.querySelector('#quality-search');
const status = root.querySelector('#quality-status');
const grade = root.querySelector('#quality-grade');
if (search?.value.trim()) {
chips.append(createChip(`Suche: ${search.value.trim()}`, () => {
search.value = '';
search.dispatchEvent(new Event('input', { bubbles: true }));
search.focus();
}));
}
if (status && status.value !== 'all') {
chips.append(createChip(`Status: ${status.options[status.selectedIndex]?.textContent || status.value}`, () => {
status.value = 'all';
status.dispatchEvent(new Event('change', { bubbles: true }));
}));
}
if (grade && grade.value !== 'all') {
chips.append(createChip(`Note: ${grade.value}`, () => {
grade.value = 'all';
grade.dispatchEvent(new Event('change', { bubbles: true }));
}));
}
chips.classList.toggle('is-empty', chips.childElementCount === 0);
}
function setStatusFilter(root, value, { focus = true } = {}) {
const select = root.querySelector('#quality-status');
if (!select) return;
select.value = value;
select.dispatchEvent(new Event('change', { bubbles: true }));
if (focus) focusNode(root.querySelector('#quality-list'));
updateFilterChips(root);
updatePriorities(root);
}
function updatePriorities(root) {
const blocked = safeNumber(root.querySelector('#quality-summary-blocked')?.textContent);
const ready = safeNumber(root.querySelector('#quality-summary-ready')?.textContent);
const total = safeNumber(root.querySelector('#quality-summary-total')?.textContent);
const review = Math.max(0, total - blocked - ready);
const counts = { blocked, review, ready, all: total };
Object.entries(counts).forEach(([status, count]) => {
const node = root.querySelector(`[data-ux-quality-count="${status}"]`);
if (node && node.textContent !== String(count)) node.textContent = String(count);
});
const selected = root.querySelector('#quality-status')?.value || 'all';
root.querySelectorAll('[data-ux-quality-status]').forEach(button => {
const active = button.dataset.uxQualityStatus === selected;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', String(active));
});
const next = root.querySelector('[data-ux-quality-next]');
if (next) {
const target = blocked > 0 ? 'blocked' : review > 0 ? 'review' : ready > 0 ? 'ready' : 'all';
const text = target === 'blocked'
? `${blocked} blockierte Artikel öffnen`
: target === 'review'
? `${review} Artikel prüfen`
: target === 'ready'
? `${ready} bereite Artikel anzeigen`
: 'Alle Artikel anzeigen';
next.dataset.uxQualityNext = target;
if (next.textContent !== text) next.textContent = text;
}
}
function groupDetailSections(root) {
const detail = root.querySelector('#quality-detail-content');
if (!detail || detail.dataset.uxQualityStructured === 'true') return;
const financials = detail.querySelector('#quality-financials');
const duplicates = detail.querySelector('.quality-duplicates');
const images = detail.querySelector('.quality-images');
const history = detail.querySelector('.quality-history');
const issueTabs = detail.querySelector('.quality-issue-tabs');
const issues = detail.querySelector('#quality-issues');
const actions = detail.querySelector('.quality-detail-actions');
if (actions) {
const main = createNode('div', 'ux-quality-main-actions');
const edit = actions.querySelector('#quality-open-article');
const ai = actions.querySelector('#quality-open-ai');
const analyze = actions.querySelector('#quality-run-analysis');
[edit, ai].filter(Boolean).forEach(button => main.append(button));
const more = createNode('details', 'ux-quality-more-actions');
const summary = createNode('summary', '', 'Weitere Aktionen');
const body = createNode('div', 'ux-quality-more-actions-body');
if (analyze) body.append(analyze);
more.append(summary, body);
actions.append(main, more);
}
if (issueTabs && issues) {
const section = createNode('section', 'ux-quality-issue-section');
const head = createNode('div', 'ux-quality-section-head');
head.append(createNode('div', '', 'Priorisierte Probleme'), createNode('span', 'ux-quality-issue-summary', 'Wird ausgewertet …'));
section.append(head, issueTabs, issues);
actions?.after(section);
}
const secondary = createNode('details', 'ux-quality-secondary');
const secondarySummary = createNode('summary');
const summaryCopy = createNode('span');
summaryCopy.append(createNode('strong', '', 'Weitere Prüfungen'), createNode('small', '', 'Erlös, Duplikate, Bilder und Score-Verlauf'));
secondarySummary.append(summaryCopy, createNode('span', 'ux-quality-secondary-badge', 'Details'));
const secondaryBody = createNode('div', 'ux-quality-secondary-body');
[financials, duplicates, images, history].filter(Boolean).forEach(node => secondaryBody.append(node));
secondary.append(secondarySummary, secondaryBody);
detail.append(secondary);
detail.dataset.uxQualityStructured = 'true';
}
function parseReadiness(root) {
const text = root.querySelector('#quality-readiness')?.textContent || '';
const blockers = safeNumber(text.match(/(\d+)\s+Blocker/i)?.[1]);
const warnings = safeNumber(text.match(/(\d+)\s+Warnungen/i)?.[1]);
const recommendations = safeNumber(text.match(/(\d+)\s+Empfehlungen/i)?.[1]);
return { blockers, warnings, recommendations };
}
function updateDetailGuidance(root) {
const detail = root.querySelector('#quality-detail-content');
if (!detail || detail.classList.contains('hidden')) return;
const { blockers, warnings, recommendations } = parseReadiness(root);
const summary = root.querySelector('.ux-quality-issue-summary');
if (summary) {
const text = blockers
? `${blockers} Blocker zuerst`
: warnings
? `${warnings} Warnung${warnings === 1 ? '' : 'en'} prüfen`
: recommendations
? `${recommendations} Empfehlung${recommendations === 1 ? '' : 'en'}`
: 'Keine offenen Punkte';
const tone = blockers ? 'danger' : warnings ? 'warning' : recommendations ? 'info' : 'success';
if (summary.textContent !== text) summary.textContent = text;
if (summary.dataset.tone !== tone) summary.dataset.tone = tone;
}
const blockerTab = root.querySelector('[data-quality-issue-filter="error"]');
const allTab = root.querySelector('[data-quality-issue-filter="all"]');
if (blockerTab) blockerTab.classList.toggle('ux-quality-has-priority', blockers > 0);
if (allTab) allTab.classList.toggle('ux-quality-all-clear', blockers === 0 && warnings === 0 && recommendations === 0);
const secondary = root.querySelector('.ux-quality-secondary');
if (secondary && blockers > 0 && secondary.open) secondary.open = false;
}
function bindInteractions(root) {
root.querySelectorAll('[data-ux-quality-status]').forEach(button => {
button.addEventListener('click', () => setStatusFilter(root, button.dataset.uxQualityStatus));
button.addEventListener('keydown', event => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
event.preventDefault();
const buttons = [...root.querySelectorAll('[data-ux-quality-status]')];
const index = buttons.indexOf(button);
const nextIndex = event.key === 'Home' ? 0
: event.key === 'End' ? buttons.length - 1
: event.key === 'ArrowRight' ? (index + 1) % buttons.length
: (index - 1 + buttons.length) % buttons.length;
buttons[nextIndex]?.focus();
});
});
root.querySelector('[data-ux-quality-next]')?.addEventListener('click', event => {
setStatusFilter(root, event.currentTarget.dataset.uxQualityNext);
});
['quality-search', 'quality-status', 'quality-grade', 'quality-page-size'].forEach(id => {
const field = root.querySelector(`#${id}`);
field?.addEventListener('input', () => updateFilterChips(root));
field?.addEventListener('change', () => {
updateFilterChips(root);
updatePriorities(root);
});
});
}
function observeState(root) {
let scheduled = false;
const schedule = () => {
if (scheduled) return;
scheduled = true;
window.requestAnimationFrame(() => {
scheduled = false;
updatePriorities(root);
updateFilterChips(root);
updateDetailGuidance(root);
});
};
const observer = new MutationObserver(schedule);
[
root.querySelector('.quality-summary-grid'),
root.querySelector('#quality-readiness'),
root.querySelector('#quality-issues'),
].filter(Boolean).forEach(node => observer.observe(node, {
subtree: true,
childList: true,
characterData: true,
attributes: true,
attributeFilter: ['class'],
}));
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
function initializeQualityCenterUx() {
const root = document.getElementById(ROOT_ID);
if (!root || root.classList.contains('ux-quality-center-ready')) return;
createPriorityBoard(root);
restructureToolbar(root);
groupDetailSections(root);
bindInteractions(root);
updatePriorities(root);
updateFilterChips(root);
updateDetailGuidance(root);
observeState(root);
root.classList.add('ux-quality-center-ready');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeQualityCenterUx, { once: true });
} else {
initializeQualityCenterUx();
}
})();
+345
View File
@@ -0,0 +1,345 @@
(() => {
'use strict';
const root = document.documentElement;
if (root.dataset.uxSafeActionAdaptersReady === '1') return;
root.dataset.uxSafeActionAdaptersReady = '1';
let lastArticleRow = null;
let lastMediaPreview = null;
const usable = element => Boolean(element
&& element.isConnected !== false
&& !element.hidden
&& !element.disabled
&& element.getAttribute?.('aria-disabled') !== 'true');
function sectionVisible(id) {
const section = document.getElementById(id);
if (!section || section.hidden || section.classList?.contains('hidden') || section.getAttribute?.('aria-hidden') === 'true') return false;
const style = typeof window.getComputedStyle === 'function' ? window.getComputedStyle(section) : null;
return !style || (style.display !== 'none' && style.visibility !== 'hidden');
}
function workspaceVisible(view, sectionId = view) {
const current = window.VendooNavigation?.currentView;
return (!current || current === view) && sectionVisible(sectionId);
}
const find = selector => {
const element = document.querySelector(selector);
return usable(element) ? element : null;
};
const click = selector => {
const element = find(selector);
if (!element || typeof element.click !== 'function') return false;
element.click();
return true;
};
function currentArticleRow() {
if (usable(lastArticleRow)) return lastArticleRow;
const row = document.querySelector('#history-list .history-row input[type="checkbox"]:checked')?.closest?.('.history-row');
return usable(row) ? row : null;
}
function currentMediaPreview() {
if (usable(lastMediaPreview)) return lastMediaPreview;
const preview = document.querySelector('#media-grid .media-card input[type="checkbox"]:checked')
?.closest?.('.media-card')?.querySelector?.('[data-media-preview]');
return usable(preview) ? preview : null;
}
function trackContext(event) {
const row = event.target?.closest?.('#history-list .history-row');
if (row) lastArticleRow = row;
const preview = event.target?.closest?.('[data-media-preview]');
if (preview) lastMediaPreview = preview;
}
document.addEventListener('focusin', trackContext, true);
document.addEventListener('pointerdown', trackContext, true);
function adapter({ id, title, description, icon, group, aliases = [], keywords = [], visible, enabled, handler }) {
return {
id,
title,
description,
icon,
group,
aliases,
keywords,
surfaces: ['command-palette'],
danger: 'none',
visible,
enabled,
handler,
};
}
function buttonAction({ selector, ...definition }) {
return adapter({
...definition,
enabled: () => Boolean(find(selector)),
handler: () => click(selector),
});
}
function registerAdapters() {
const actions = window.VendooActions;
if (!actions || root.dataset.uxSafeActionAdaptersRegistered === '1') return false;
const moduleVisible = () => workspaceVisible('admin', 'admin-modules-panel');
const extensionVisible = () => workspaceVisible('extensions');
const articleVisible = () => workspaceVisible('history');
const mediaVisible = () => workspaceVisible('media-library');
const registered = [
buttonAction({
id: 'workspace:modules:refresh',
title: 'Module neu laden',
description: 'Die vorhandene Modulübersicht mit ihrem bestehenden Refresh-Handler aktualisieren.',
icon: '↻',
group: 'Module & Extensions',
aliases: ['Module aktualisieren'],
keywords: ['module', 'refresh', 'neu laden'],
visible: moduleVisible,
selector: '#module-manager-refresh',
}),
buttonAction({
id: 'workspace:modules:filter-all',
title: 'Alle Module anzeigen',
description: 'Den vorhandenen Modulfilter auf alle Einträge setzen.',
icon: '◫',
group: 'Module & Extensions',
keywords: ['module', 'filter', 'alle'],
visible: moduleVisible,
selector: '[data-ux-module-group="modules"][data-ux-module-filter="all"]',
}),
buttonAction({
id: 'workspace:modules:filter-active',
title: 'Aktive Module anzeigen',
description: 'Den vorhandenen Modulfilter auf aktive Module setzen.',
icon: '✓',
group: 'Module & Extensions',
keywords: ['module', 'aktiv', 'filter'],
visible: moduleVisible,
selector: '[data-ux-module-group="modules"][data-ux-module-filter="active"]',
}),
buttonAction({
id: 'workspace:modules:filter-disabled',
title: 'Deaktivierte Module anzeigen',
description: 'Den vorhandenen Modulfilter auf deaktivierte Module setzen.',
icon: '',
group: 'Module & Extensions',
keywords: ['module', 'deaktiviert', 'filter'],
visible: moduleVisible,
selector: '[data-ux-module-group="modules"][data-ux-module-filter="disabled"]',
}),
buttonAction({
id: 'workspace:modules:filter-external',
title: 'Installierte Module anzeigen',
description: 'Den vorhandenen Modulfilter auf installierte externe Module setzen.',
icon: '▣',
group: 'Module & Extensions',
keywords: ['module', 'installiert', 'extern', 'filter'],
visible: moduleVisible,
selector: '[data-ux-module-group="modules"][data-ux-module-filter="external"]',
}),
buttonAction({
id: 'workspace:modules:filter-problems',
title: 'Module mit Problemen anzeigen',
description: 'Den vorhandenen Modulfilter auf fehlerhafte oder ungültige Module setzen.',
icon: '!',
group: 'Module & Extensions',
aliases: ['Problematische Module', 'Modulfehler'],
keywords: ['module', 'probleme', 'fehler'],
visible: moduleVisible,
selector: '[data-ux-module-group="modules"][data-ux-module-filter="problems"]',
}),
buttonAction({
id: 'workspace:extensions:filter-all',
title: 'Alle Extensions anzeigen',
description: 'Den vorhandenen Extension-Filter auf alle Browser-Erweiterungen setzen.',
icon: '◫',
group: 'Module & Extensions',
keywords: ['extensions', 'browser', 'filter', 'alle'],
visible: extensionVisible,
selector: '[data-ux-module-group="extensions"][data-ux-module-filter="all"]',
}),
buttonAction({
id: 'workspace:extensions:filter-ready',
title: 'Bereite Extensions anzeigen',
description: 'Den vorhandenen Extension-Filter auf vorbereitete oder installierte Erweiterungen setzen.',
icon: '✓',
group: 'Module & Extensions',
keywords: ['extensions', 'bereit', 'vorbereitet', 'filter'],
visible: extensionVisible,
selector: '[data-ux-module-group="extensions"][data-ux-module-filter="ready"]',
}),
buttonAction({
id: 'workspace:extensions:filter-connected',
title: 'Verbundene Extensions anzeigen',
description: 'Den vorhandenen Extension-Filter auf verbundene Browser-Erweiterungen setzen.',
icon: '↔',
group: 'Module & Extensions',
keywords: ['extensions', 'verbunden', 'browser', 'filter'],
visible: extensionVisible,
selector: '[data-ux-module-group="extensions"][data-ux-module-filter="connected"]',
}),
buttonAction({
id: 'workspace:extensions:filter-attention',
title: 'Extensions mit Handlungsbedarf anzeigen',
description: 'Den vorhandenen Extension-Filter auf Einträge mit notwendiger Aktion setzen.',
icon: '!',
group: 'Module & Extensions',
aliases: ['Extension-Probleme', 'Aktion nötig'],
keywords: ['extensions', 'aktion', 'problem', 'aufmerksamkeit'],
visible: extensionVisible,
selector: '[data-ux-module-group="extensions"][data-ux-module-filter="attention"]',
}),
adapter({
id: 'workspace:articles:open-current',
title: 'Aktuellen Artikel öffnen',
description: 'Den zuletzt fokussierten oder ausgewählten Artikel mit dem bestehenden Handler öffnen.',
icon: '↗',
group: 'Artikel',
aliases: ['Artikel öffnen', 'Ausgewählten Artikel öffnen'],
keywords: ['artikel', 'öffnen', 'auswahl'],
visible: articleVisible,
enabled: () => Boolean(currentArticleRow()?.querySelector?.('.history-article-title')),
handler: () => {
const title = currentArticleRow()?.querySelector?.('.history-article-title');
if (!usable(title) || typeof title.click !== 'function') return false;
title.click();
return true;
},
}),
buttonAction({
id: 'workspace:articles:view-cards',
title: 'Artikel als Karten anzeigen',
description: 'Die bestehende Kartenansicht der Artikelübersicht aktivieren.',
icon: '▦',
group: 'Artikel',
aliases: ['Kartenansicht'],
keywords: ['artikel', 'karten', 'ansicht'],
visible: articleVisible,
selector: '[data-article-view-choice="cards"]',
}),
buttonAction({
id: 'workspace:articles:view-list',
title: 'Artikel als Liste anzeigen',
description: 'Die bestehende Listenansicht der Artikelübersicht aktivieren.',
icon: '☷',
group: 'Artikel',
aliases: ['Listenansicht'],
keywords: ['artikel', 'liste', 'ansicht'],
visible: articleVisible,
selector: '[data-article-view-choice="list"]',
}),
buttonAction({
id: 'workspace:articles:reset-filters',
title: 'Artikelfilter zurücksetzen',
description: 'Den vorhandenen Reset-Handler der Artikelübersicht ausführen.',
icon: '×',
group: 'Artikel',
keywords: ['artikel', 'filter', 'zurücksetzen', 'reset'],
visible: articleVisible,
selector: '#history-reset-filters',
}),
adapter({
id: 'workspace:media:open-current-preview',
title: 'Aktuelles Bild groß öffnen',
description: 'Das zuletzt fokussierte oder ausgewählte Bild mit der bestehenden Vorschau öffnen.',
icon: '⌕',
group: 'Medien',
aliases: ['Bildvorschau öffnen', 'Bild groß anzeigen'],
keywords: ['bild', 'medien', 'vorschau', 'lightbox'],
visible: mediaVisible,
enabled: () => Boolean(currentMediaPreview()),
handler: () => {
const preview = currentMediaPreview();
if (!preview || typeof preview.click !== 'function') return false;
preview.click();
return true;
},
}),
buttonAction({
id: 'workspace:media:density-compact',
title: 'Galerie kompakt anzeigen',
description: 'Die bestehende kompakte Kartengröße der Medienbibliothek aktivieren.',
icon: '▦',
group: 'Medien',
aliases: ['Kleine Bildkarten'],
keywords: ['medien', 'galerie', 'kompakt', 'klein', 'dichte'],
visible: mediaVisible,
selector: '[data-media-density-option="compact"]',
}),
buttonAction({
id: 'workspace:media:density-comfortable',
title: 'Galerie normal anzeigen',
description: 'Die bestehende normale Kartengröße der Medienbibliothek aktivieren.',
icon: '▦',
group: 'Medien',
aliases: ['Mittlere Bildkarten'],
keywords: ['medien', 'galerie', 'normal', 'dichte'],
visible: mediaVisible,
selector: '[data-media-density-option="comfortable"]',
}),
buttonAction({
id: 'workspace:media:density-large',
title: 'Galerie groß anzeigen',
description: 'Die bestehende große Kartendarstellung der Medienbibliothek aktivieren.',
icon: '▦',
group: 'Medien',
aliases: ['Große Bildkarten'],
keywords: ['medien', 'galerie', 'groß', 'dichte'],
visible: mediaVisible,
selector: '[data-media-density-option="large"]',
}),
buttonAction({
id: 'workspace:media:view-grid',
title: 'Medien als Raster anzeigen',
description: 'Die bestehende Rasteransicht der Medienbibliothek aktivieren.',
icon: '▦',
group: 'Medien',
aliases: ['Galerieraster'],
keywords: ['medien', 'raster', 'grid', 'ansicht'],
visible: mediaVisible,
selector: '#media-view-grid',
}),
buttonAction({
id: 'workspace:media:view-list',
title: 'Medien als Liste anzeigen',
description: 'Die bestehende Listenansicht der Medienbibliothek aktivieren.',
icon: '☷',
group: 'Medien',
aliases: ['Medienliste'],
keywords: ['medien', 'liste', 'ansicht'],
visible: mediaVisible,
selector: '#media-view-list',
}),
buttonAction({
id: 'workspace:media:reset-filters',
title: 'Medienfilter zurücksetzen',
description: 'Den vorhandenen Filter-Reset der Medienbibliothek ausführen.',
icon: '×',
group: 'Medien',
keywords: ['medien', 'filter', 'zurücksetzen'],
visible: mediaVisible,
selector: '.ux-media-filter-reset',
}),
];
actions.registerMany(registered);
root.dataset.uxSafeActionAdaptersRegistered = '1';
actions.refresh();
window.dispatchEvent(new CustomEvent('vendoo:safe-action-adapters-ready', {
detail: { count: registered.length, baselineCount: 9, expansionCount: registered.length - 9 },
}));
return true;
}
if (!registerAdapters()) window.addEventListener('vendoo:actions-ready', registerAdapters, { once: true });
})();
+39
View File
@@ -0,0 +1,39 @@
#settings.ux-settings-control-center-ready{--ux-settings-gap:clamp(14px,2vw,24px)}
#settings .ux-settings-command{display:grid;grid-template-columns:minmax(280px,1.35fr) minmax(320px,1fr);gap:var(--ux-settings-gap);padding:clamp(16px,2.3vw,26px);margin:0 0 var(--ux-settings-gap);border:1px solid var(--vd-border);border-radius:var(--vd-radius-lg,18px);background:var(--vd-surface);box-shadow:var(--vd-shadow-sm)}
#settings .ux-settings-search{display:grid;gap:8px;align-content:start}
#settings .ux-settings-search>span{font-size:.75rem;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--vd-text-muted)}
#settings .ux-settings-search input{width:100%;min-height:50px;padding:0 16px;border:1px solid var(--vd-border-strong,var(--vd-border));border-radius:var(--vd-radius-md,12px);background:var(--vd-input-bg,var(--vd-surface));color:var(--vd-text);font:inherit}
#settings .ux-settings-search input:focus{outline:3px solid color-mix(in srgb,var(--vd-primary) 20%,transparent);border-color:var(--vd-primary)}
#settings .ux-settings-search-result{min-height:22px;font-size:.86rem;color:var(--vd-text-muted)}
#settings .ux-settings-health{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
#settings .ux-settings-health-card{display:grid;grid-template-columns:auto 1fr;gap:10px;align-items:center;text-align:left;padding:12px;border:1px solid var(--vd-border);border-radius:var(--vd-radius-md,12px);background:var(--vd-surface-subtle,var(--vd-surface));color:var(--vd-text);cursor:pointer}
#settings .ux-settings-health-card:hover{border-color:var(--vd-primary)}
#settings .ux-settings-health-card small,#settings .ux-settings-health-card strong{display:block}
#settings .ux-settings-health-card small{font-size:.72rem;color:var(--vd-text-muted)}
#settings .ux-settings-health-card strong{margin-top:2px;font-size:.82rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#settings .ux-settings-health-dot{width:10px;height:10px;border-radius:50%;background:var(--vd-text-muted);box-shadow:0 0 0 4px color-mix(in srgb,var(--vd-text-muted) 13%,transparent)}
#settings .ux-settings-health-card[data-tone="success"] .ux-settings-health-dot{background:var(--vd-success);box-shadow:0 0 0 4px color-mix(in srgb,var(--vd-success) 14%,transparent)}
#settings .ux-settings-health-card[data-tone="warning"] .ux-settings-health-dot{background:var(--vd-warning);box-shadow:0 0 0 4px color-mix(in srgb,var(--vd-warning) 14%,transparent)}
#settings .ux-settings-health-card[data-tone="danger"] .ux-settings-health-dot{background:var(--vd-danger);box-shadow:0 0 0 4px color-mix(in srgb,var(--vd-danger) 14%,transparent)}
#settings .ux-settings-category-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px;margin:0 0 var(--ux-settings-gap)}
#settings .ux-settings-category-card{display:grid;gap:7px;min-height:142px;padding:15px;text-align:left;border:1px solid var(--vd-border);border-radius:var(--vd-radius-md,14px);background:var(--vd-surface);color:var(--vd-text);cursor:pointer}
#settings .ux-settings-category-card:hover,#settings .ux-settings-category-card[aria-pressed="true"]{border-color:var(--vd-primary);box-shadow:0 0 0 2px color-mix(in srgb,var(--vd-primary) 11%,transparent)}
#settings .ux-settings-category-index{font-size:.72rem;font-weight:800;color:var(--vd-primary)}
#settings .ux-settings-category-card strong{font-size:.94rem}
#settings .ux-settings-category-card small{font-size:.76rem;line-height:1.4;color:var(--vd-text-muted)}
#settings .ux-settings-category-card b{margin-top:auto;font-size:.76rem;color:var(--vd-primary)}
#settings .settings-shell{grid-template-columns:minmax(190px,230px) minmax(0,1fr)}
#settings .settings-nav{position:sticky;top:88px;align-self:start}
#settings .ux-settings-advanced{margin-top:18px;border:1px solid var(--vd-border);border-radius:var(--vd-radius-md,14px);background:var(--vd-surface)}
#settings .ux-settings-advanced>summary{display:flex;justify-content:space-between;gap:16px;align-items:center;padding:15px 17px;cursor:pointer;list-style:none}
#settings .ux-settings-advanced>summary::-webkit-details-marker{display:none}
#settings .ux-settings-advanced>summary small{color:var(--vd-text-muted)}
#settings .ux-settings-advanced[open]>summary{border-bottom:1px solid var(--vd-border)}
#settings .ux-settings-advanced>*:not(summary){margin:16px}
#settings .ux-settings-search-hit{outline:3px solid color-mix(in srgb,var(--vd-warning) 32%,transparent);outline-offset:6px;border-radius:6px}
#settings .settings-savebar{position:sticky;bottom:12px;z-index:20;box-shadow:var(--vd-shadow-lg)}
@media (max-width:1100px){#settings .ux-settings-category-grid{grid-template-columns:repeat(3,minmax(0,1fr))}#settings .settings-shell{grid-template-columns:1fr}#settings .settings-nav{position:static;display:flex;overflow-x:auto;padding-bottom:4px}#settings .settings-nav-item{min-width:190px}}
@media (max-width:780px){#settings .ux-settings-command{grid-template-columns:1fr}#settings .ux-settings-category-grid{grid-template-columns:repeat(2,minmax(0,1fr))}#settings .settings-hero{align-items:flex-start}.settings-hero-status{width:100%}}
@media (max-width:520px){#settings .ux-settings-health,#settings .ux-settings-category-grid{grid-template-columns:1fr}#settings .ux-settings-category-card{min-height:auto}#settings .ux-settings-advanced>summary{align-items:flex-start;flex-direction:column;gap:4px}#settings .settings-savebar{bottom:0;border-radius:0}}
@media (prefers-reduced-motion:reduce){#settings *{scroll-behavior:auto!important;transition:none!important}}
#settings button:focus-visible,#settings input:focus-visible,#settings select:focus-visible,#settings textarea:focus-visible,#settings summary:focus-visible{outline:3px solid color-mix(in srgb,var(--vd-primary) 28%,transparent);outline-offset:2px}
+205
View File
@@ -0,0 +1,205 @@
(() => {
'use strict';
const root = document.getElementById('settings');
if (!root || root.dataset.uxSettingsReady === '1') return;
root.dataset.uxSettingsReady = '1';
root.classList.add('ux-settings-control-center-ready');
const storageKey = 'vendoo_settings_control_center_v1';
const panelButtons = [...root.querySelectorAll('[data-settings-panel]')];
const panels = [...root.querySelectorAll('[data-settings-panel-content]')];
const hero = root.querySelector('.settings-hero');
const shell = root.querySelector('.settings-shell');
if (!hero || !shell || !panelButtons.length || !panels.length) return;
const state = { query: '', lastPanel: 'general' };
try { Object.assign(state, JSON.parse(localStorage.getItem(storageKey) || '{}')); } catch {}
const create = (tag, className, text) => {
const element = document.createElement(tag);
if (className) element.className = className;
if (text !== undefined) element.textContent = text;
return element;
};
const command = create('section', 'ux-settings-command');
command.setAttribute('aria-label', 'Einstellungen durchsuchen und Systemstatus prüfen');
const searchWrap = create('label', 'ux-settings-search');
searchWrap.setAttribute('for', 'ux-settings-search-input');
const searchTitle = create('span', '', 'Wo möchtest du etwas ändern?');
const search = create('input');
search.id = 'ux-settings-search-input';
search.type = 'search';
search.placeholder = 'Zum Beispiel eBay, Theme, OpenRouter, FLUX oder Sprache';
search.autocomplete = 'off';
search.value = state.query || '';
searchWrap.append(searchTitle, search);
const result = create('div', 'ux-settings-search-result');
result.id = 'ux-settings-search-result';
result.setAttribute('role', 'status');
result.setAttribute('aria-live', 'polite');
const health = create('div', 'ux-settings-health');
health.setAttribute('aria-label', 'Systemstatus');
const healthItems = [
['ai', 'AI-Dienste', '#settings-ai-status-text'],
['marketplaces', 'Marktplätze', '#etsy-status-badge, #ebay-status-badge'],
['flux', 'Lokales FLUX', '#settings-flux-status-text, #local-flux-engine-title'],
['general', 'Netzwerk', '#public-base-url-status'],
];
const healthCards = healthItems.map(([panel, label, selector]) => {
const button = create('button', 'ux-settings-health-card');
button.type = 'button';
button.dataset.uxSettingsHealth = panel;
const dot = create('span', 'ux-settings-health-dot');
const copy = create('span');
copy.append(create('small', '', label), create('strong', '', 'Wird geprüft …'));
button.append(dot, copy);
button.addEventListener('click', () => activatePanel(panel, { focus: true }));
health.append(button);
return { button, value: copy.querySelector('strong'), selector };
});
command.append(searchWrap, result, health);
hero.insertAdjacentElement('afterend', command);
const categoryGrid = create('section', 'ux-settings-category-grid');
categoryGrid.setAttribute('aria-label', 'Einstellungsbereiche');
const categoryCopy = {
general: ['Allgemein', 'Standards, Sprache, Netzwerk und Verkäuferhinweise'],
appearance: ['Darstellung', 'Theme, Dichte, Animationen und Kontrast'],
ai: ['AI & APIs', 'Text- und Bildprovider, Modelle und Sicherheitsoptionen'],
marketplaces: ['Marktplätze', 'eBay- und Etsy-Verbindungen kontrollieren'],
flux: ['Lokales FLUX', 'ComfyUI, Leistung, Installation und Wartung'],
};
panelButtons.forEach((navButton, index) => {
const panel = navButton.dataset.settingsPanel;
const [title, copy] = categoryCopy[panel] || [panel, 'Einstellungen öffnen'];
const card = create('button', 'ux-settings-category-card');
card.type = 'button';
card.dataset.uxSettingsCategory = panel;
card.setAttribute('aria-pressed', index === 0 ? 'true' : 'false');
card.append(create('span', 'ux-settings-category-index', String(index + 1).padStart(2, '0')),
create('strong', '', title), create('small', '', copy), create('b', '', 'Öffnen →'));
card.addEventListener('click', () => activatePanel(panel, { focus: true }));
categoryGrid.append(card);
});
command.insertAdjacentElement('afterend', categoryGrid);
const advancedSelectors = [
'.theme-admin-only', '.reference-control-card', '.local-flux-status-panel',
'.settings-disclosure', '.local-flux-install-options', '.local-flux-actions'
];
panels.forEach(panel => {
const advanced = advancedSelectors.flatMap(selector => [...panel.querySelectorAll(selector)]);
if (!advanced.length || panel.querySelector(':scope > .ux-settings-advanced')) return;
const details = create('details', 'ux-settings-advanced');
const summary = create('summary');
summary.append(create('strong', '', 'Erweiterte Einstellungen'), create('small', '', 'Technische und selten benötigte Optionen'));
details.append(summary);
advanced.forEach(element => details.append(element));
panel.append(details);
});
const normalize = value => String(value || '').toLocaleLowerCase('de-DE').normalize('NFD').replace(/[\u0300-\u036f]/g, '');
const searchable = panels.map(panel => ({
panel,
id: panel.dataset.settingsPanelContent,
text: normalize(panel.textContent),
targets: [...panel.querySelectorAll('label, .settings-card-head strong, .settings-panel-head h3')]
.map(element => ({ element, text: normalize(element.textContent) }))
}));
function markActive(id) {
state.lastPanel = id;
try { localStorage.setItem(storageKey, JSON.stringify(state)); } catch {}
root.querySelectorAll('[data-ux-settings-category]').forEach(card => {
card.setAttribute('aria-pressed', card.dataset.uxSettingsCategory === id ? 'true' : 'false');
});
}
function activatePanel(id, { focus = false } = {}) {
const nav = panelButtons.find(button => button.dataset.settingsPanel === id);
if (!nav) return;
nav.click();
markActive(id);
if (focus) {
const panel = panels.find(item => item.dataset.settingsPanelContent === id);
window.requestAnimationFrame(() => panel?.querySelector('input, select, textarea, button')?.focus({ preventScroll: true }));
}
}
function clearHighlights() {
root.querySelectorAll('.ux-settings-search-hit').forEach(element => element.classList.remove('ux-settings-search-hit'));
}
function applySearch() {
const query = normalize(search.value.trim());
state.query = search.value.trim();
try { localStorage.setItem(storageKey, JSON.stringify(state)); } catch {}
clearHighlights();
if (query.length < 2) {
result.textContent = query ? 'Bitte mindestens zwei Zeichen eingeben.' : 'Suche über alle Einstellungsbereiche.';
return;
}
const matches = searchable.filter(entry => entry.text.includes(query));
if (!matches.length) {
result.textContent = `Keine Einstellung zu „${search.value.trim()}“ gefunden.`;
return;
}
const first = matches[0];
activatePanel(first.id);
const target = first.targets.find(item => item.text.includes(query))?.element || first.panel.querySelector('.settings-panel-head');
target?.classList.add('ux-settings-search-hit');
target?.scrollIntoView({ behavior: document.documentElement.dataset.reducedMotion === 'true' ? 'auto' : 'smooth', block: 'center' });
result.textContent = `${matches.length} Bereich${matches.length === 1 ? '' : 'e'} gefunden · ${categoryCopy[first.id]?.[0] || first.id} geöffnet.`;
}
let searchTimer;
search.addEventListener('input', () => {
window.clearTimeout(searchTimer);
searchTimer = window.setTimeout(applySearch, 180);
});
search.addEventListener('keydown', event => {
if (event.key === 'Escape') {
search.value = '';
applySearch();
search.focus();
}
});
function statusTone(text) {
const value = normalize(text);
if (/fehler|nicht|ungultig|deaktiviert|fehlgeschlagen|nicht erreichbar/.test(value)) return 'danger';
if (/wird|pruf|unbekannt|noch keine|gestoppt/.test(value)) return 'warning';
if (/bereit|verbunden|aktiv|erreichbar|ok|gesetzt/.test(value)) return 'success';
return 'neutral';
}
function refreshHealth() {
healthCards.forEach(item => {
const sources = [...root.querySelectorAll(item.selector)].filter(element => element.textContent.trim());
const text = sources.map(element => element.textContent.trim()).join(' · ') || 'Noch nicht geprüft';
item.value.textContent = text.slice(0, 72);
item.button.dataset.tone = statusTone(text);
});
}
const observed = [
'#settings-ai-status-text', '#settings-flux-status-text', '#etsy-status-badge', '#ebay-status-badge',
'#local-flux-engine-title', '#public-base-url-status'
].flatMap(selector => [...root.querySelectorAll(selector)]);
const observer = new MutationObserver(() => window.requestAnimationFrame(refreshHealth));
observed.forEach(element => observer.observe(element, { childList: true, characterData: true, subtree: true, attributes: true, attributeFilter: ['class'] }));
panelButtons.forEach(button => button.addEventListener('click', () => markActive(button.dataset.settingsPanel)));
activatePanel(state.lastPanel || 'general');
refreshHealth();
applySearch();
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
})();
+485
View File
@@ -0,0 +1,485 @@
#templates.ux-template-workspace-ready {
display: grid;
gap: var(--vd-space-5, 20px);
}
#templates .ux-template-hero {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
align-items: center;
gap: var(--vd-space-5, 20px);
padding: clamp(20px, 3vw, 34px);
border: 1px solid var(--vd-color-border, var(--border));
border-radius: var(--vd-radius-xl, 20px);
background: linear-gradient(135deg, color-mix(in srgb, var(--vd-color-surface, var(--surface)) 94%, var(--vd-color-accent, var(--accent)) 6%), var(--vd-color-surface, var(--surface)));
box-shadow: var(--vd-shadow-sm, 0 8px 24px rgba(15, 23, 42, .06));
}
#templates .ux-template-hero-copy {
min-width: 0;
}
#templates .ux-template-kicker {
display: block;
margin-bottom: 6px;
color: var(--vd-color-accent, var(--accent));
font-size: .72rem;
font-weight: 800;
letter-spacing: .09em;
text-transform: uppercase;
}
#templates .ux-template-hero h2,
#templates .ux-template-editor-head h3 {
margin: 0;
color: var(--vd-color-text, var(--text));
}
#templates .ux-template-hero p,
#templates .ux-template-editor-head p {
max-width: 720px;
margin: 8px 0 0;
color: var(--vd-color-text-muted, var(--text-muted));
line-height: 1.55;
}
#templates .ux-template-metrics {
display: flex;
gap: 10px;
}
#templates .ux-template-metric {
min-width: 112px;
padding: 12px 14px;
border: 1px solid var(--vd-color-border, var(--border));
border-radius: var(--vd-radius-lg, 14px);
background: color-mix(in srgb, var(--vd-color-surface, var(--surface)) 88%, transparent);
}
#templates .ux-template-metric strong,
#templates .ux-template-metric span {
display: block;
}
#templates .ux-template-metric strong {
font-size: 1.35rem;
line-height: 1;
}
#templates .ux-template-metric span {
margin-top: 5px;
color: var(--vd-color-text-muted, var(--text-muted));
font-size: .72rem;
}
#templates .ux-template-primary-action {
white-space: nowrap;
}
#templates .ux-template-toolbar {
display: grid;
grid-template-columns: minmax(240px, 1fr) auto;
align-items: end;
gap: 16px;
padding: 16px;
}
#templates .ux-template-search,
#templates .ux-template-filter-controls label {
display: grid;
gap: 6px;
}
#templates .ux-template-search > span,
#templates .ux-template-filter-controls label > span {
color: var(--vd-color-text-muted, var(--text-muted));
font-size: .72rem;
font-weight: 700;
}
#templates .ux-template-search input,
#templates .ux-template-filter-controls select {
width: 100%;
min-height: 42px;
border: 1px solid var(--vd-color-border, var(--border));
border-radius: var(--vd-radius-md, 10px);
background: var(--vd-color-surface, var(--surface));
color: var(--vd-color-text, var(--text));
}
#templates .ux-template-filter-controls {
display: flex;
align-items: end;
gap: 10px;
}
#templates .ux-template-result-count {
min-width: 92px;
padding: 11px 12px;
border-radius: 999px;
background: var(--vd-color-surface-muted, var(--surface-2));
color: var(--vd-color-text-muted, var(--text-muted));
font-size: .78rem;
font-weight: 700;
text-align: center;
}
#templates #template-list.template-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 310px), 1fr));
gap: 14px;
}
#templates .ux-template-card {
display: grid;
align-content: space-between;
min-height: 174px;
overflow: visible;
border: 1px solid var(--vd-color-border, var(--border));
border-radius: var(--vd-radius-lg, 15px);
background: var(--vd-color-surface, var(--surface));
box-shadow: var(--vd-shadow-xs, 0 3px 12px rgba(15, 23, 42, .05));
transition: transform .16s ease, border-color .16s ease, box-shadow .16s ease;
}
#templates .ux-template-card:hover {
transform: translateY(-2px);
border-color: color-mix(in srgb, var(--vd-color-accent, var(--accent)) 42%, var(--vd-color-border, var(--border)));
box-shadow: var(--vd-shadow-sm, 0 8px 24px rgba(15, 23, 42, .08));
}
#templates .ux-template-card-head {
display: grid;
gap: 16px;
padding: 18px;
}
#templates .ux-template-card-identity {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
#templates .ux-template-card-mark {
display: grid;
place-items: center;
flex: 0 0 42px;
width: 42px;
height: 42px;
border-radius: 12px;
background: color-mix(in srgb, var(--vd-color-accent, var(--accent)) 13%, var(--vd-color-surface, var(--surface)));
color: var(--vd-color-accent, var(--accent));
font-size: .78rem;
font-weight: 850;
letter-spacing: .04em;
}
#templates .ux-template-card .tpl-info {
min-width: 0;
}
#templates .ux-template-card .tpl-name {
overflow: hidden;
color: var(--vd-color-text, var(--text));
font-size: 1rem;
font-weight: 760;
text-overflow: ellipsis;
white-space: nowrap;
}
#templates .ux-template-card .tpl-meta {
margin-top: 4px;
color: var(--vd-color-text-muted, var(--text-muted));
font-size: .76rem;
}
#templates .ux-template-badges {
display: flex;
flex-wrap: wrap;
gap: 7px;
}
#templates .ux-template-badge {
display: inline-flex;
align-items: center;
min-height: 26px;
padding: 4px 9px;
border: 1px solid var(--vd-color-border, var(--border));
border-radius: 999px;
background: var(--vd-color-surface-muted, var(--surface-2));
color: var(--vd-color-text-muted, var(--text-muted));
font-size: .69rem;
font-weight: 720;
}
#templates .ux-template-badge.is-universal {
border-color: color-mix(in srgb, var(--vd-color-accent, var(--accent)) 35%, var(--vd-color-border, var(--border)));
color: var(--vd-color-accent, var(--accent));
}
#templates .ux-template-badge.is-language {
min-width: 34px;
justify-content: center;
font-weight: 850;
}
#templates .ux-template-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 12px 14px;
border-top: 1px solid var(--vd-color-border, var(--border));
background: color-mix(in srgb, var(--vd-color-surface-muted, var(--surface-2)) 62%, transparent);
}
#templates .ux-template-edit {
flex: 1 1 auto;
justify-content: center;
}
#templates .ux-template-more-actions {
position: relative;
}
#templates .ux-template-more-actions summary {
min-height: 34px;
padding: 8px 10px;
border-radius: 9px;
color: var(--vd-color-text-muted, var(--text-muted));
cursor: pointer;
font-size: .72rem;
font-weight: 700;
list-style: none;
}
#templates .ux-template-more-actions summary::-webkit-details-marker {
display: none;
}
#templates .ux-template-more-actions[open] summary {
background: var(--vd-color-surface, var(--surface));
}
#templates .ux-template-more-actions-body {
position: absolute;
right: 0;
bottom: calc(100% + 7px);
z-index: 8;
min-width: 150px;
padding: 8px;
border: 1px solid var(--vd-color-border, var(--border));
border-radius: 11px;
background: var(--vd-color-surface, var(--surface));
box-shadow: var(--vd-shadow-md, 0 16px 38px rgba(15, 23, 42, .16));
}
#templates .ux-template-more-actions-body button {
width: 100%;
}
#templates .ux-template-filtered-out {
display: none !important;
}
#templates.ux-template-editor-open .ux-template-toolbar,
#templates.ux-template-editor-open #template-list,
#templates.ux-template-editor-open .ux-template-metrics,
#templates.ux-template-editor-open .ux-template-primary-action {
display: none !important;
}
#templates.ux-template-list-open #template-form {
display: none;
}
#templates.ux-template-no-results #template-list::after {
content: 'Keine Vorlage passt zu den gewählten Filtern.';
display: block;
grid-column: 1 / -1;
padding: 28px;
border: 1px dashed var(--vd-color-border, var(--border));
border-radius: 14px;
color: var(--vd-color-text-muted, var(--text-muted));
text-align: center;
}
#templates #template-form.ux-template-form,
#templates #template-form {
width: 100%;
max-width: none;
padding: 0;
border: 0;
background: transparent;
}
#templates .ux-template-editor-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
padding: clamp(20px, 3vw, 30px);
border: 1px solid var(--vd-color-border, var(--border));
border-radius: var(--vd-radius-xl, 20px);
background: var(--vd-color-surface, var(--surface));
}
#templates .ux-template-editor-status {
max-width: 320px;
padding: 9px 12px;
border-radius: 999px;
background: color-mix(in srgb, var(--vd-color-success, #16835a) 12%, var(--vd-color-surface, var(--surface)));
color: var(--vd-color-success, #16835a);
font-size: .76rem;
font-weight: 750;
text-align: right;
}
#templates .ux-template-editor-status.is-incomplete {
background: color-mix(in srgb, var(--vd-color-warning, #b76a00) 12%, var(--vd-color-surface, var(--surface)));
color: var(--vd-color-warning, #9a5a00);
}
#templates .ux-template-editor-layout {
display: grid;
grid-template-columns: minmax(260px, .82fr) minmax(340px, 1.18fr);
gap: 16px;
margin-top: 16px;
}
#templates .ux-template-editor-section {
display: grid;
align-content: start;
gap: 16px;
padding: 20px;
}
#templates .ux-template-section-heading {
padding-bottom: 12px;
border-bottom: 1px solid var(--vd-color-border, var(--border));
color: var(--vd-color-text, var(--text));
font-size: .84rem;
font-weight: 800;
}
#templates .ux-template-editor-section .setting-group {
margin: 0;
}
#templates .ux-template-editor-section input,
#templates .ux-template-editor-section select {
width: 100%;
}
#templates .ux-template-content-section #tpl-notes-editor {
min-height: 180px !important;
}
#templates .ux-template-editor-actions {
position: sticky;
bottom: 14px;
z-index: 7;
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 16px;
padding: 12px;
border: 1px solid var(--vd-color-border, var(--border));
border-radius: 14px;
background: color-mix(in srgb, var(--vd-color-surface, var(--surface)) 94%, transparent);
box-shadow: var(--vd-shadow-md, 0 14px 36px rgba(15, 23, 42, .13));
backdrop-filter: blur(12px);
}
#templates :focus-visible {
outline: 3px solid color-mix(in srgb, var(--vd-color-accent, var(--accent)) 42%, transparent);
outline-offset: 2px;
}
@media (max-width: 920px) {
#templates .ux-template-hero {
grid-template-columns: 1fr auto;
}
#templates .ux-template-metrics {
grid-column: 1 / -1;
grid-row: 2;
}
#templates .ux-template-editor-layout {
grid-template-columns: 1fr;
}
}
@media (max-width: 700px) {
#templates .ux-template-hero,
#templates .ux-template-toolbar {
grid-template-columns: 1fr;
}
#templates .ux-template-primary-action {
width: 100%;
}
#templates .ux-template-filter-controls {
display: grid;
grid-template-columns: 1fr 1fr;
}
#templates .ux-template-result-count {
grid-column: 1 / -1;
}
#templates .ux-template-editor-head {
display: grid;
}
#templates .ux-template-editor-status {
max-width: none;
text-align: left;
}
}
@media (max-width: 470px) {
#templates .ux-template-metrics,
#templates .ux-template-filter-controls {
display: grid;
grid-template-columns: 1fr;
}
#templates .ux-template-metric {
min-width: 0;
}
#templates #template-list.template-list {
grid-template-columns: 1fr;
}
#templates .ux-template-card-footer,
#templates .ux-template-editor-actions {
align-items: stretch;
flex-direction: column;
}
#templates .ux-template-more-actions,
#templates .ux-template-more-actions summary,
#templates .ux-template-editor-actions button {
width: 100%;
}
#templates .ux-template-more-actions-body {
right: auto;
left: 0;
}
}
@media (prefers-reduced-motion: reduce) {
#templates .ux-template-card {
transition: none;
}
#templates .ux-template-card:hover {
transform: none;
}
}
+349
View File
@@ -0,0 +1,349 @@
(() => {
'use strict';
const ROOT_ID = 'templates';
const FILTER_KEY = 'vendoo_template_workspace_filters';
const createNode = (tag, className = '', text = '') => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text) node.textContent = text;
return node;
};
const normalize = value => String(value || '').trim().toLocaleLowerCase('de-DE');
function readStoredFilters() {
try {
const parsed = JSON.parse(localStorage.getItem(FILTER_KEY) || 'null');
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
function persistFilters(root) {
try {
localStorage.setItem(FILTER_KEY, JSON.stringify({
platform: root.querySelector('[data-ux-template-platform]')?.value || 'all',
language: root.querySelector('[data-ux-template-language]')?.value || 'all',
}));
} catch {}
}
function createHero(root) {
if (root.querySelector('.ux-template-hero')) return;
const header = root.querySelector('.templates-header');
const addButton = root.querySelector('#add-template-btn');
if (!header || !addButton) return;
const hero = createNode('section', 'ux-template-hero');
const copy = createNode('div', 'ux-template-hero-copy');
copy.append(
createNode('span', 'ux-template-kicker', 'Wiederverwendbare Listing-Basis'),
createNode('h2', '', 'Vorlagen schneller finden und sauber pflegen'),
createNode('p', '', 'Plattform, Sprache, Verkäuferhinweise und Standard-Tags zentral vorbereiten und bei neuen Artikeln wiederverwenden.')
);
const metrics = createNode('div', 'ux-template-metrics');
const total = createNode('article', 'ux-template-metric');
const totalValue = createNode('strong', '', '0');
totalValue.dataset.uxTemplateMetric = 'total';
total.append(totalValue, createNode('span', '', 'Vorlagen'));
const universal = createNode('article', 'ux-template-metric');
const universalValue = createNode('strong', '', '0');
universalValue.dataset.uxTemplateMetric = 'universal';
universal.append(universalValue, createNode('span', '', 'Für alle Plattformen'));
metrics.append(total, universal);
addButton.textContent = 'Neue Vorlage erstellen';
addButton.classList.add('ux-template-primary-action');
hero.append(copy, metrics, addButton);
header.replaceWith(hero);
}
function createToolbar(root) {
if (root.querySelector('.ux-template-toolbar')) return;
const list = root.querySelector('#template-list');
if (!list) return;
const stored = readStoredFilters();
const toolbar = createNode('section', 'ux-template-toolbar card');
const searchLabel = createNode('label', 'ux-template-search');
searchLabel.append(createNode('span', '', 'Vorlagen suchen'));
const search = createNode('input');
search.type = 'search';
search.placeholder = 'Name oder Plattform …';
search.dataset.uxTemplateSearch = 'true';
searchLabel.append(search);
const controls = createNode('div', 'ux-template-filter-controls');
const platformLabel = createNode('label');
platformLabel.append(createNode('span', '', 'Plattform'));
const platform = createNode('select');
platform.dataset.uxTemplatePlatform = 'true';
platform.dataset.uxTemplateInitial = stored.platform || 'all';
platform.append(new Option('Alle Plattformen', 'all'));
platformLabel.append(platform);
const languageLabel = createNode('label');
languageLabel.append(createNode('span', '', 'Sprache'));
const language = createNode('select');
language.dataset.uxTemplateLanguage = 'true';
language.append(
new Option('Alle Sprachen', 'all'),
new Option('Deutsch', 'DE'),
new Option('English', 'EN'),
new Option('Français', 'FR')
);
language.value = ['DE', 'EN', 'FR'].includes(stored.language) ? stored.language : 'all';
languageLabel.append(language);
const result = createNode('span', 'ux-template-result-count', '0 Vorlagen');
result.setAttribute('aria-live', 'polite');
controls.append(platformLabel, languageLabel, result);
toolbar.append(searchLabel, controls);
list.before(toolbar);
search.addEventListener('input', () => applyFilters(root));
platform.addEventListener('change', () => {
persistFilters(root);
applyFilters(root);
});
language.addEventListener('change', () => {
persistFilters(root);
applyFilters(root);
});
}
function parseTemplateMeta(item) {
const metaText = item.querySelector('.tpl-meta')?.textContent?.trim() || '';
const parts = metaText.split('·').map(part => part.trim()).filter(Boolean);
const platform = parts[0] || 'Alle Plattformen';
const language = (parts[1] || 'DE').toUpperCase();
return {
name: item.querySelector('.tpl-name')?.textContent?.trim() || 'Unbenannte Vorlage',
platform,
language,
universal: normalize(platform).includes('alle plattform'),
};
}
function syncPlatformOptions(root) {
const select = root.querySelector('[data-ux-template-platform]');
if (!select) return;
const initial = select.dataset.uxTemplateInitial;
const current = initial || select.value || readStoredFilters().platform || 'all';
const platforms = [...new Set([...root.querySelectorAll('#template-list .template-item')]
.map(item => parseTemplateMeta(item).platform)
.filter(Boolean))]
.sort((a, b) => a.localeCompare(b, 'de'));
const expected = ['all', ...platforms];
const present = [...select.options].map(option => option.value);
if (expected.join('\u0000') !== present.join('\u0000')) {
select.replaceChildren(new Option('Alle Plattformen', 'all'));
platforms.forEach(value => select.append(new Option(value, value)));
}
select.value = expected.includes(current) ? current : 'all';
delete select.dataset.uxTemplateInitial;
}
function enhanceTemplateCards(root) {
root.querySelectorAll('#template-list .template-item').forEach(item => {
const meta = parseTemplateMeta(item);
item.dataset.uxTemplateName = normalize(meta.name);
item.dataset.uxTemplatePlatform = meta.platform;
item.dataset.uxTemplateLanguage = meta.language;
item.dataset.uxTemplateUniversal = meta.universal ? 'true' : 'false';
if (item.dataset.uxTemplateEnhanced === 'true') return;
item.dataset.uxTemplateEnhanced = 'true';
item.classList.add('ux-template-card');
const info = item.querySelector('.tpl-info');
const name = item.querySelector('.tpl-name');
const metaNode = item.querySelector('.tpl-meta');
const actions = item.querySelector('.tpl-actions');
if (!info || !name || !metaNode || !actions) return;
const cardHead = createNode('div', 'ux-template-card-head');
const identity = createNode('div', 'ux-template-card-identity');
const mark = createNode('span', 'ux-template-card-mark', meta.name.slice(0, 2).toUpperCase());
identity.append(mark, info);
const badges = createNode('div', 'ux-template-badges');
badges.append(
createNode('span', meta.universal ? 'ux-template-badge is-universal' : 'ux-template-badge', meta.platform),
createNode('span', 'ux-template-badge is-language', meta.language)
);
cardHead.append(identity, badges);
const edit = actions.querySelector('.tpl-edit');
const remove = actions.querySelector('.tpl-delete');
const footer = createNode('div', 'ux-template-card-footer');
if (edit) {
edit.classList.add('vd-button', 'compact', 'ux-template-edit');
edit.textContent = 'Vorlage bearbeiten';
footer.append(edit);
}
if (remove) {
const more = createNode('details', 'ux-template-more-actions');
const summary = createNode('summary', '', 'Weitere Aktionen');
const body = createNode('div', 'ux-template-more-actions-body');
remove.classList.add('danger');
body.append(remove);
more.append(summary, body);
footer.append(more);
}
actions.remove();
item.replaceChildren(cardHead, footer);
});
}
function updateMetrics(root) {
const cards = [...root.querySelectorAll('#template-list .template-item')];
const total = root.querySelector('[data-ux-template-metric="total"]');
const universal = root.querySelector('[data-ux-template-metric="universal"]');
if (total) total.textContent = String(cards.length);
if (universal) universal.textContent = String(cards.filter(card => card.dataset.uxTemplateUniversal === 'true').length);
}
function applyFilters(root) {
syncPlatformOptions(root);
const search = normalize(root.querySelector('[data-ux-template-search]')?.value);
const platform = root.querySelector('[data-ux-template-platform]')?.value || 'all';
const language = root.querySelector('[data-ux-template-language]')?.value || 'all';
let visible = 0;
root.querySelectorAll('#template-list .template-item').forEach(item => {
const haystack = `${item.dataset.uxTemplateName || ''} ${normalize(item.dataset.uxTemplatePlatform)}`;
const matchesSearch = !search || haystack.includes(search);
const matchesPlatform = platform === 'all' || item.dataset.uxTemplatePlatform === platform;
const matchesLanguage = language === 'all' || item.dataset.uxTemplateLanguage === language;
const show = matchesSearch && matchesPlatform && matchesLanguage;
item.classList.toggle('ux-template-filtered-out', !show);
if (show) visible += 1;
});
const count = root.querySelector('.ux-template-result-count');
if (count) count.textContent = `${visible} ${visible === 1 ? 'Vorlage' : 'Vorlagen'}`;
root.classList.toggle('ux-template-no-results', visible === 0 && root.querySelectorAll('#template-list .template-item').length > 0);
updateMetrics(root);
}
function updateEditorStatus(root) {
const status = root.querySelector('.ux-template-editor-status');
if (!status) return;
const name = root.querySelector('#tpl-name')?.value.trim();
const platform = root.querySelector('#tpl-platform')?.selectedOptions?.[0]?.textContent || 'Alle Plattformen';
const language = root.querySelector('#tpl-language')?.selectedOptions?.[0]?.textContent || 'Deutsch';
status.textContent = name ? `${name} · ${platform} · ${language}` : 'Name fehlt noch';
status.classList.toggle('is-incomplete', !name);
}
function structureEditor(root) {
const form = root.querySelector('#template-form');
if (!form || form.dataset.uxTemplateStructured === 'true') return;
const title = form.querySelector('#template-form-title');
const groups = [...form.querySelectorAll(':scope > .setting-group')];
const actions = form.querySelector(':scope > .result-actions');
if (!title || groups.length < 5 || !actions) return;
const head = createNode('header', 'ux-template-editor-head');
const copy = createNode('div');
copy.append(createNode('span', 'ux-template-kicker', 'Vorlagen-Editor'), title, createNode('p', '', 'Lege die wiederverwendbare Grundlage fest. Änderungen werden erst über „Vorlage speichern“ übernommen.'));
const status = createNode('span', 'ux-template-editor-status', 'Entwurf');
status.setAttribute('aria-live', 'polite');
head.append(copy, status);
const layout = createNode('div', 'ux-template-editor-layout');
const basics = createNode('section', 'ux-template-editor-section card');
basics.append(createNode('div', 'ux-template-section-heading', 'Grundlage'));
groups.slice(0, 3).forEach(group => basics.append(group));
const content = createNode('section', 'ux-template-editor-section card ux-template-content-section');
content.append(createNode('div', 'ux-template-section-heading', 'Inhalte & Standardwerte'));
groups.slice(3).forEach(group => content.append(group));
layout.append(basics, content);
const save = actions.querySelector('#save-template-btn');
const cancel = actions.querySelector('#cancel-template-btn');
if (save) save.textContent = 'Vorlage speichern';
actions.classList.add('ux-template-editor-actions');
if (cancel) actions.prepend(cancel);
form.replaceChildren(head, layout, actions);
form.dataset.uxTemplateStructured = 'true';
['tpl-name', 'tpl-platform', 'tpl-language', 'tpl-tags'].forEach(id => {
root.querySelector(`#${id}`)?.addEventListener('input', () => updateEditorStatus(root));
root.querySelector(`#${id}`)?.addEventListener('change', () => updateEditorStatus(root));
});
updateEditorStatus(root);
}
function syncMode(root) {
const form = root.querySelector('#template-form');
const open = Boolean(form && !form.classList.contains('hidden'));
root.classList.toggle('ux-template-editor-open', open);
root.classList.toggle('ux-template-list-open', !open);
if (open) {
updateEditorStatus(root);
requestAnimationFrame(() => form.scrollIntoView({ behavior: 'smooth', block: 'start' }));
}
}
function enhance(root) {
createHero(root);
createToolbar(root);
structureEditor(root);
enhanceTemplateCards(root);
applyFilters(root);
syncMode(root);
root.classList.add('ux-template-workspace-ready');
}
function setupKeyboardNavigation(root) {
const toolbar = root.querySelector('.ux-template-toolbar');
toolbar?.addEventListener('keydown', event => {
if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
const controls = [...toolbar.querySelectorAll('input, select')].filter(control => !control.disabled);
const index = controls.indexOf(document.activeElement);
if (index < 0) return;
event.preventDefault();
const direction = event.key === 'ArrowRight' ? 1 : -1;
controls[(index + direction + controls.length) % controls.length]?.focus();
});
}
function init() {
const root = document.getElementById(ROOT_ID);
if (!root || root.dataset.uxTemplateWorkspace === 'true') return;
root.dataset.uxTemplateWorkspace = 'true';
enhance(root);
setupKeyboardNavigation(root);
let scheduled = false;
const schedule = () => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
enhance(root);
});
};
const observer = new MutationObserver(mutations => {
if (mutations.some(mutation => mutation.type === 'childList' || (mutation.type === 'attributes' && mutation.attributeName === 'class'))) schedule();
});
const list = root.querySelector('#template-list');
const form = root.querySelector('#template-form');
if (list) observer.observe(list, { childList: true });
if (form) observer.observe(form, { attributes: true, attributeFilter: ['class'] });
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true });
else init();
})();
+375
View File
@@ -0,0 +1,375 @@
#today.ux-today-workspace-ready {
display: grid;
gap: var(--vd-space-5, 20px);
}
#today .ux-today-command {
display: grid;
gap: 18px;
padding: clamp(20px, 3vw, 32px);
border: 1px solid var(--vd-color-border, var(--border));
border-radius: var(--vd-radius-xl, 20px);
background: linear-gradient(135deg, color-mix(in srgb, var(--vd-color-surface, var(--surface)) 91%, var(--vd-color-accent, var(--accent)) 9%), var(--vd-color-surface, var(--surface)));
box-shadow: var(--vd-shadow-sm, 0 8px 26px rgba(15, 23, 42, .07));
}
#today[data-ux-today-priority="attention"] .ux-today-command {
background: linear-gradient(135deg, color-mix(in srgb, var(--vd-color-surface, var(--surface)) 89%, var(--vd-color-danger, #c63d35) 11%), var(--vd-color-surface, var(--surface)));
}
#today[data-ux-today-priority="ready"] .ux-today-command {
background: linear-gradient(135deg, color-mix(in srgb, var(--vd-color-surface, var(--surface)) 90%, var(--vd-color-success, #16835a) 10%), var(--vd-color-surface, var(--surface)));
}
#today .ux-today-command-main {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 22px;
}
#today .ux-today-command-copy {
min-width: 0;
}
#today .ux-today-date {
display: block;
margin-bottom: 7px;
color: var(--vd-color-accent, var(--accent));
font-size: .72rem;
font-weight: 800;
letter-spacing: .08em;
text-transform: uppercase;
}
#today .ux-today-command h3 {
margin: 0;
color: var(--vd-color-text, var(--text));
font-size: clamp(1.35rem, 2.4vw, 2rem);
line-height: 1.15;
}
#today .ux-today-command p {
max-width: 760px;
margin: 9px 0 0;
color: var(--vd-color-text-muted, var(--text-muted));
line-height: 1.55;
}
#today .ux-today-command-actions {
display: grid;
justify-items: end;
gap: 10px;
}
#today .ux-today-primary {
min-width: 190px;
justify-content: center;
}
#today .ux-today-shortcuts {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 7px;
}
#today .ux-today-shortcut {
min-height: 34px;
padding: 7px 10px;
border: 1px solid var(--vd-color-border, var(--border));
border-radius: 9px;
background: color-mix(in srgb, var(--vd-color-surface, var(--surface)) 86%, transparent);
color: var(--vd-color-text-muted, var(--text-muted));
font: inherit;
font-size: .73rem;
font-weight: 720;
cursor: pointer;
}
#today .ux-today-shortcut:hover {
border-color: color-mix(in srgb, var(--vd-color-accent, var(--accent)) 44%, var(--vd-color-border, var(--border)));
color: var(--vd-color-text, var(--text));
}
#today .ux-today-command .today-summary-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 10px;
margin: 0;
}
#today .ux-today-command .today-summary-grid button {
min-height: 82px;
padding: 12px;
border: 1px solid var(--vd-color-border, var(--border));
border-radius: var(--vd-radius-lg, 14px);
background: color-mix(in srgb, var(--vd-color-surface, var(--surface)) 88%, transparent);
box-shadow: none;
}
#today .ux-today-command .today-summary-grid button:hover,
#today .ux-today-command .today-summary-grid button.active {
border-color: color-mix(in srgb, var(--vd-color-accent, var(--accent)) 48%, var(--vd-color-border, var(--border)));
transform: translateY(-1px);
}
#today .ux-today-command .today-summary-grid strong {
font-size: 1.25rem;
}
#today .ux-today-command .today-summary-grid em {
display: none;
}
#today .ux-today-filter-details {
overflow: hidden;
border: 1px solid var(--vd-color-border, var(--border));
border-radius: var(--vd-radius-lg, 14px);
background: var(--vd-color-surface, var(--surface));
}
#today .ux-today-filter-details > summary {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 48px;
padding: 12px 16px;
color: var(--vd-color-text, var(--text));
cursor: pointer;
font-size: .82rem;
font-weight: 780;
list-style: none;
}
#today .ux-today-filter-details > summary::-webkit-details-marker {
display: none;
}
#today .ux-today-filter-details > summary::after {
content: '▾';
color: var(--vd-color-text-muted, var(--text-muted));
transition: transform .16s ease;
}
#today .ux-today-filter-details[open] > summary::after {
transform: rotate(180deg);
}
#today .ux-today-filter-details > summary span {
margin-left: auto;
margin-right: 12px;
color: var(--vd-color-text-muted, var(--text-muted));
font-size: .72rem;
font-weight: 600;
}
#today .ux-today-filter-details .today-filterbar {
margin: 0;
padding: 0 16px 16px;
border: 0;
background: transparent;
}
#today .studio-workspace {
display: grid;
gap: 16px;
}
#today .ux-today-flow-contents {
display: contents;
}
#today .ux-today-priority-attention {
order: 1;
}
#today .ux-today-priority-ready {
order: 2;
}
#today .ux-today-secondary-scheduled {
order: 3;
}
#today .ux-today-secondary-activity {
order: 4;
}
#today.ux-today-no-attention .ux-today-priority-attention {
display: none;
}
#today .work-panel {
border-radius: var(--vd-radius-lg, 15px);
}
#today.ux-today-has-attention .ux-today-priority-attention {
border-color: color-mix(in srgb, var(--vd-color-danger, #c63d35) 45%, var(--vd-color-border, var(--border)));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--vd-color-danger, #c63d35) 10%, transparent);
}
#today .today-ready-panel {
min-height: 180px;
}
#today .ux-today-secondary-scheduled,
#today .ux-today-secondary-activity {
background: color-mix(in srgb, var(--vd-color-surface, var(--surface)) 96%, var(--vd-color-surface-muted, var(--surface-2)) 4%);
}
#today .ux-today-panel-toggle {
min-height: 32px;
margin-left: auto;
padding: 6px 9px;
border: 1px solid var(--vd-color-border, var(--border));
border-radius: 8px;
background: var(--vd-color-surface, var(--surface));
color: var(--vd-color-text-muted, var(--text-muted));
font: inherit;
font-size: .7rem;
font-weight: 720;
cursor: pointer;
}
#today .ux-today-panel-collapsed {
min-height: 0;
}
#today .ux-today-panel-collapsed > :not(.work-panel-header) {
display: none !important;
}
#today .studio-layout-v2 {
grid-template-columns: minmax(0, 1fr) minmax(290px, 340px);
align-items: start;
}
#today .studio-inspector {
position: sticky;
top: calc(var(--topbar-height, 72px) + 18px);
max-height: calc(100vh - var(--topbar-height, 72px) - 36px);
overflow: auto;
}
#today .ux-today-inspector-toggle {
display: none;
width: 100%;
justify-content: center;
}
#today .today-ready-grid,
#today .today-activity-grid {
gap: 12px;
}
#today .today-table-wrap {
overflow-x: auto;
}
#today .ux-today-command button:focus-visible,
#today .ux-today-filter-details summary:focus-visible,
#today .ux-today-panel-toggle:focus-visible,
#today .ux-today-inspector-toggle:focus-visible {
outline: 3px solid color-mix(in srgb, var(--vd-color-accent, var(--accent)) 45%, transparent);
outline-offset: 2px;
}
@media (max-width: 1100px) {
#today .ux-today-command-main {
grid-template-columns: 1fr;
}
#today .ux-today-command-actions {
justify-items: start;
}
#today .ux-today-shortcuts {
justify-content: flex-start;
}
#today .ux-today-command .today-summary-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 900px) {
#today .studio-layout-v2 {
grid-template-columns: 1fr;
}
#today .ux-today-inspector-toggle {
display: inline-flex;
}
#today .studio-inspector {
position: static;
display: none;
max-height: none;
}
#today.ux-today-inspector-open .studio-inspector {
display: block;
}
}
@media (max-width: 700px) {
#today .ux-today-command {
padding: 18px;
}
#today .ux-today-command .today-summary-grid {
display: flex;
overflow-x: auto;
padding-bottom: 4px;
scroll-snap-type: x proximity;
}
#today .ux-today-command .today-summary-grid button {
flex: 0 0 132px;
scroll-snap-align: start;
}
#today .today-filterbar,
#today .today-filter-tabs,
#today .today-activity-grid {
grid-template-columns: 1fr;
}
#today .ux-today-filter-details > summary span {
display: none;
}
}
@media (max-width: 470px) {
#today .ux-today-command-actions,
#today .ux-today-primary {
width: 100%;
}
#today .ux-today-shortcuts {
display: grid;
grid-template-columns: 1fr;
width: 100%;
}
#today .ux-today-shortcut {
width: 100%;
}
#today .work-panel-header {
align-items: flex-start;
flex-wrap: wrap;
}
}
@media (prefers-reduced-motion: reduce) {
#today *,
#today *::before,
#today *::after {
scroll-behavior: auto !important;
transition-duration: .01ms !important;
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
}
}
+286
View File
@@ -0,0 +1,286 @@
(() => {
'use strict';
const ROOT_ID = 'today';
const PANEL_STATE_KEY = 'vendoo_today_workspace_panels';
const createNode = (tag, className = '', text = '') => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text) node.textContent = text;
return node;
};
const countValue = id => {
const value = Number.parseInt(document.getElementById(id)?.textContent || '0', 10);
return Number.isFinite(value) ? Math.max(0, value) : 0;
};
function readPanelState() {
try {
const parsed = JSON.parse(localStorage.getItem(PANEL_STATE_KEY) || 'null');
return parsed && typeof parsed === 'object' ? parsed : {};
} catch {
return {};
}
}
function savePanelState(state) {
try { localStorage.setItem(PANEL_STATE_KEY, JSON.stringify(state)); } catch {}
}
function focusTodayFilter(root, filter) {
const target = root.querySelector(`.today-filter-tabs [data-today-filter="${filter}"]`)
|| root.querySelector(`.today-summary-grid [data-today-filter="${filter}"]`);
target?.click();
}
function scrollToPanel(panel) {
panel?.scrollIntoView({ behavior: 'smooth', block: 'start' });
window.setTimeout(() => panel?.querySelector('button, input, select, [tabindex="0"]')?.focus({ preventScroll: true }), 180);
}
function createCommandCenter(root) {
if (root.querySelector('.ux-today-command')) return;
const heading = root.querySelector('.studio-heading');
const summary = root.querySelector('.today-summary-grid');
if (!heading || !summary) return;
const command = createNode('section', 'ux-today-command');
const copy = createNode('div', 'ux-today-command-copy');
const date = createNode('span', 'ux-today-date');
date.textContent = new Intl.DateTimeFormat('de-DE', { weekday: 'long', day: '2-digit', month: 'long' }).format(new Date());
const title = createNode('h3', '', 'Tagesarbeit wird vorbereitet');
title.dataset.uxTodayTitle = 'true';
const description = createNode('p', '', 'Vendoo priorisiert offene Aufgaben, Veröffentlichungen und Artikel für dich.');
description.dataset.uxTodayCopy = 'true';
copy.append(date, title, description);
const actions = createNode('div', 'ux-today-command-actions');
const primary = createNode('button', 'vd-button primary ux-today-primary', 'Nächsten Schritt öffnen');
primary.type = 'button';
primary.dataset.uxTodayPrimary = 'true';
actions.append(primary);
const shortcuts = createNode('div', 'ux-today-shortcuts');
const shortcutData = [
['generator', 'Neuer Artikel'],
['quality-center', 'Qualität prüfen'],
['publish', 'Publish-Center'],
];
shortcutData.forEach(([target, label]) => {
const button = createNode('button', 'ux-today-shortcut', label);
button.type = 'button';
button.dataset.tabTarget = target;
shortcuts.append(button);
});
actions.append(shortcuts);
const main = createNode('div', 'ux-today-command-main');
main.append(copy, actions);
command.append(main, summary);
heading.after(command);
primary.addEventListener('click', () => runPrimaryAction(root));
}
function structureFilters(root) {
if (root.querySelector('.ux-today-filter-details')) return;
const filterbar = root.querySelector('.today-filterbar');
if (!filterbar) return;
const details = createNode('details', 'ux-today-filter-details');
const summary = createNode('summary', '', 'Ansicht & Sortierung');
const hint = createNode('span', '', 'Filter und Reihenfolge anpassen');
summary.append(hint);
filterbar.before(details);
details.append(summary, filterbar);
}
function createPanelToggle(panel, key, label, defaultCollapsed) {
if (!panel || panel.querySelector('[data-ux-today-panel-toggle]')) return;
const header = panel.querySelector('.work-panel-header');
if (!header) return;
const stored = readPanelState();
const collapsed = typeof stored[key] === 'boolean' ? stored[key] : defaultCollapsed;
panel.classList.toggle('ux-today-panel-collapsed', collapsed);
const button = createNode('button', 'ux-today-panel-toggle', collapsed ? `${label} öffnen` : `${label} einklappen`);
button.type = 'button';
button.dataset.uxTodayPanelToggle = key;
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
header.append(button);
button.addEventListener('click', () => {
const nextCollapsed = !panel.classList.contains('ux-today-panel-collapsed');
panel.classList.toggle('ux-today-panel-collapsed', nextCollapsed);
button.textContent = nextCollapsed ? `${label} öffnen` : `${label} einklappen`;
button.setAttribute('aria-expanded', nextCollapsed ? 'false' : 'true');
const next = readPanelState();
next[key] = nextCollapsed;
savePanelState(next);
});
}
function structurePanels(root) {
const workspace = root.querySelector('.studio-workspace');
const middle = root.querySelector('.today-middle-grid');
const ready = root.querySelector('.today-ready-panel');
const scheduled = root.querySelector('.today-scheduled-panel');
const attention = root.querySelector('.attention-panel');
const activity = root.querySelector('.today-activity-panel');
if (!workspace || !middle || !ready || !scheduled || !attention || !activity) return;
middle.classList.add('ux-today-flow-contents');
attention.classList.add('ux-today-priority-attention');
ready.classList.add('ux-today-priority-ready');
scheduled.classList.add('ux-today-secondary-scheduled');
activity.classList.add('ux-today-secondary-activity');
createPanelToggle(scheduled, 'scheduled', 'Planungen', countValue('today-count-scheduled') === 0);
createPanelToggle(activity, 'activity', 'Aktivität', true);
}
function createInspectorToggle(root) {
if (root.querySelector('[data-ux-today-inspector-toggle]')) return;
const inspector = root.querySelector('#studio-inspector');
const layout = root.querySelector('.studio-layout-v2');
if (!inspector || !layout) return;
const toggle = createNode('button', 'vd-button ux-today-inspector-toggle', 'Artikelkontext anzeigen');
toggle.type = 'button';
toggle.dataset.uxTodayInspectorToggle = 'true';
toggle.setAttribute('aria-controls', 'studio-inspector');
toggle.setAttribute('aria-expanded', 'false');
layout.before(toggle);
toggle.addEventListener('click', () => {
const open = !root.classList.contains('ux-today-inspector-open');
root.classList.toggle('ux-today-inspector-open', open);
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
toggle.textContent = open ? 'Artikelkontext schließen' : 'Artikelkontext anzeigen';
if (open) inspector.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
}
function runPrimaryAction(root) {
const attention = countValue('today-count-attention');
const ready = countValue('today-count-ready');
const scheduled = countValue('today-count-scheduled');
if (attention > 0) {
focusTodayFilter(root, 'attention');
scrollToPanel(root.querySelector('.attention-panel'));
return;
}
if (ready > 0) {
focusTodayFilter(root, 'ready');
scrollToPanel(root.querySelector('.today-ready-panel'));
return;
}
if (scheduled > 0) {
const panel = root.querySelector('.today-scheduled-panel');
panel?.classList.remove('ux-today-panel-collapsed');
const toggle = panel?.querySelector('[data-ux-today-panel-toggle]');
toggle?.setAttribute('aria-expanded', 'true');
if (toggle) toggle.textContent = 'Planungen einklappen';
focusTodayFilter(root, 'scheduled');
scrollToPanel(panel);
return;
}
document.getElementById('topbar-new-btn')?.click();
}
function updateCommandCenter(root) {
const counts = {
ready: countValue('today-count-ready'),
scheduled: countValue('today-count-scheduled'),
published: countValue('today-count-published'),
sold: countValue('today-count-sold'),
attention: countValue('today-count-attention'),
};
const title = root.querySelector('[data-ux-today-title]');
const copy = root.querySelector('[data-ux-today-copy]');
const primary = root.querySelector('[data-ux-today-primary]');
const inspectorContent = root.querySelector('#studio-inspector-content');
const inspectorToggle = root.querySelector('[data-ux-today-inspector-toggle]');
root.classList.toggle('ux-today-has-attention', counts.attention > 0);
root.classList.toggle('ux-today-no-attention', counts.attention === 0);
root.dataset.uxTodayPriority = counts.attention > 0 ? 'attention' : counts.ready > 0 ? 'ready' : counts.scheduled > 0 ? 'scheduled' : 'clear';
if (counts.attention > 0) {
if (title) title.textContent = `${counts.attention} ${counts.attention === 1 ? 'Problem zuerst klären' : 'Probleme zuerst klären'}`;
if (copy) copy.textContent = 'Blocker und fehlerhafte Vorgänge stehen heute vor Routinearbeit.';
if (primary) primary.textContent = 'Probleme anzeigen';
} else if (counts.ready > 0) {
if (title) title.textContent = `${counts.ready} ${counts.ready === 1 ? 'Artikel ist' : 'Artikel sind'} bereit`;
if (copy) copy.textContent = 'Diese Listings können geprüft, veröffentlicht oder weiterbearbeitet werden.';
if (primary) primary.textContent = 'Bereite Artikel prüfen';
} else if (counts.scheduled > 0) {
if (title) title.textContent = `${counts.scheduled} ${counts.scheduled === 1 ? 'Veröffentlichung läuft' : 'Veröffentlichungen laufen'}`;
if (copy) copy.textContent = 'Aktuell gibt es keine Blocker. Behalte nur die geplanten Vorgänge im Blick.';
if (primary) primary.textContent = 'Planungen öffnen';
} else {
if (title) title.textContent = 'Deine Tagesarbeit ist aufgeräumt';
if (copy) copy.textContent = `${counts.published} live · ${counts.sold} verkauft. Du kannst direkt mit einem neuen Artikel starten.`;
if (primary) primary.textContent = 'Neuen Artikel erstellen';
}
root.querySelectorAll('.today-summary-grid [data-today-filter]').forEach(button => {
const filter = button.dataset.todayFilter || 'all';
const value = filter === 'ready' ? counts.ready
: filter === 'scheduled' ? counts.scheduled
: filter === 'published' ? counts.published
: filter === 'sold' ? counts.sold
: filter === 'attention' ? counts.attention : 0;
button.setAttribute('aria-label', `${button.querySelector('small')?.textContent || filter}: ${value}`);
});
const hasContext = Boolean(inspectorContent && !inspectorContent.classList.contains('hidden'));
root.classList.toggle('ux-today-has-context', hasContext);
if (inspectorToggle && !root.classList.contains('ux-today-inspector-open')) {
inspectorToggle.textContent = hasContext ? 'Ausgewählten Artikel anzeigen' : 'Artikelkontext anzeigen';
}
}
function setupKeyboardNavigation(root) {
const controls = [...root.querySelectorAll('.today-summary-grid [data-today-filter]')];
controls.forEach(button => button.addEventListener('keydown', event => {
if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
const index = controls.indexOf(button);
if (index < 0) return;
event.preventDefault();
const direction = event.key === 'ArrowRight' ? 1 : -1;
controls[(index + direction + controls.length) % controls.length]?.focus();
}));
}
function observeTodayState(root) {
const targets = [
'today-count-ready', 'today-count-scheduled', 'today-count-published', 'today-count-sold',
'today-count-attention', 'attention-count', 'today-ready-total', 'today-scheduled-total',
].map(id => document.getElementById(id)).filter(Boolean);
const inspectorContent = root.querySelector('#studio-inspector-content');
if (inspectorContent) targets.push(inspectorContent);
const observer = new MutationObserver(() => window.requestAnimationFrame(() => updateCommandCenter(root)));
targets.forEach(target => observer.observe(target, { childList: true, characterData: true, subtree: true, attributes: target === inspectorContent, attributeFilter: target === inspectorContent ? ['class'] : undefined }));
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
function initTodayWorkspace() {
const root = document.getElementById(ROOT_ID);
if (!root || root.dataset.uxTodayWorkspace === 'ready') return;
createCommandCenter(root);
structureFilters(root);
structurePanels(root);
createInspectorToggle(root);
setupKeyboardNavigation(root);
updateCommandCenter(root);
observeTodayState(root);
root.dataset.uxTodayWorkspace = 'ready';
root.classList.add('ux-today-workspace-ready');
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initTodayWorkspace, { once: true });
else initTodayWorkspace();
})();
+360
View File
@@ -0,0 +1,360 @@
/* Vendoo UX Foundation 1.44.0
* Klare Navigation, reduzierte Dashboard-Komplexität und bessere responsive Hierarchie.
*/
:root {
--ux-sidebar-width: 252px;
--ux-content-max: 1440px;
}
.ux-shell-ready .sidebar {
width: var(--ux-sidebar-width);
}
.ux-shell-ready .main-wrapper {
margin-left: var(--ux-sidebar-width);
}
.ux-shell-ready .sidebar-header {
padding: 18px 16px 14px;
}
.ux-shell-ready .sidebar-nav {
padding: 10px 8px 16px;
}
.ux-nav-sections {
display: flex;
flex-direction: column;
gap: 14px;
}
.ux-nav-section {
display: flex;
flex-direction: column;
gap: 4px;
}
.ux-nav-section-label,
.ux-nav-section > summary {
color: var(--text-muted);
font-size: 10px;
font-weight: 700;
letter-spacing: .09em;
line-height: 1.2;
padding: 0 10px 5px;
text-transform: uppercase;
}
.ux-nav-section > summary {
align-items: center;
cursor: pointer;
display: flex;
justify-content: space-between;
list-style: none;
min-height: 28px;
}
.ux-nav-section > summary::-webkit-details-marker {
display: none;
}
.ux-nav-section > summary::after {
content: '';
font-size: 16px;
transform: rotate(90deg);
transition: transform .16s ease;
}
.ux-nav-section:not([open]) > summary::after {
transform: rotate(0deg);
}
.ux-nav-section-items {
display: flex;
flex-direction: column;
gap: 3px;
}
.ux-shell-ready .nav-item {
border: 1px solid transparent;
border-radius: var(--vd-radius-md);
font-size: 13px;
min-height: 40px;
padding: 9px 11px;
}
.ux-shell-ready .nav-item svg {
height: 18px;
opacity: .75;
width: 18px;
}
.ux-shell-ready .nav-item.active {
border-color: color-mix(in srgb, var(--accent) 22%, transparent);
box-shadow: inset 3px 0 0 var(--accent);
}
.ux-shell-ready .nav-item:focus-visible,
.ux-shell-ready .ux-command-action:focus-visible,
.ux-shell-ready .command-search:focus-visible {
outline: 3px solid var(--accent-light);
outline-offset: 2px;
}
.ux-shell-ready main {
max-width: var(--ux-content-max);
padding: 26px 32px 48px;
}
.ux-shell-ready .topbar {
min-height: 64px;
padding: 12px 32px;
}
.ux-shell-ready .topbar-heading {
min-width: 180px;
}
.ux-shell-ready .topbar-title {
font-size: 17px;
}
.ux-shell-ready .topbar-kicker {
display: block;
font-size: 11px;
margin-top: 2px;
}
.ux-shell-ready .command-search {
max-width: 480px;
min-width: 280px;
}
.ux-shell-ready .tab-content.active > .page-heading h2,
.ux-shell-ready .tab-content.active > .settings-hero h2 {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
white-space: nowrap;
width: 1px;
}
.ux-shell-ready .tab-content.active > .page-heading,
.ux-shell-ready .tab-content.active > .settings-hero {
margin-bottom: 20px;
}
.ux-shell-ready .tab-content.active > .page-heading .eyebrow {
display: none;
}
.ux-command-center {
align-items: center;
background: linear-gradient(135deg, var(--bg-card), var(--bg-page));
border: 1px solid var(--border);
border-radius: var(--vd-radius-xl);
display: grid;
gap: 22px;
grid-template-columns: minmax(0, 1fr) auto;
margin-bottom: 22px;
padding: 22px 24px;
}
.ux-command-copy span {
color: var(--accent);
display: block;
font-size: 11px;
font-weight: 700;
letter-spacing: .08em;
margin-bottom: 7px;
text-transform: uppercase;
}
.ux-command-copy h3 {
font-size: clamp(20px, 2vw, 27px);
letter-spacing: -.035em;
line-height: 1.15;
margin: 0 0 7px;
}
.ux-command-copy p {
color: var(--text-secondary);
line-height: 1.55;
margin: 0;
max-width: 680px;
}
.ux-command-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
}
.ux-command-action {
align-items: center;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--vd-radius-md);
color: var(--text);
cursor: pointer;
display: inline-flex;
font: inherit;
font-size: 13px;
font-weight: 650;
gap: 7px;
min-height: 40px;
padding: 9px 13px;
}
.ux-command-action:hover {
border-color: var(--accent);
color: var(--accent);
}
.ux-command-action.primary {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.ux-command-action.primary:hover {
background: var(--accent-hover);
color: white;
}
.ux-dashboard-more {
border-top: 1px solid var(--border);
margin-top: 12px;
padding-top: 8px;
}
.ux-dashboard-more > summary {
color: var(--text-secondary);
cursor: pointer;
font-size: 12px;
font-weight: 700;
list-style: none;
padding: 8px 2px;
}
.ux-dashboard-more > summary::-webkit-details-marker {
display: none;
}
.ux-dashboard-more > summary::after {
color: var(--text-muted);
content: ' anzeigen';
font-weight: 500;
}
.ux-dashboard-more[open] > summary::after {
content: ' ausblenden';
}
.ux-dashboard-more-content {
display: grid;
gap: 14px;
padding-top: 8px;
}
.ux-shell-ready .dashboard-actions-card {
display: none;
}
.ux-shell-ready .dashboard-kpis {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.ux-shell-ready .dashboard-card,
.ux-shell-ready .work-panel {
border-radius: var(--vd-radius-lg);
}
@media (max-width: 1180px) {
.ux-shell-ready .dashboard-layout {
grid-template-columns: 1fr;
}
.ux-shell-ready .dashboard-rail {
display: grid;
gap: 14px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ux-dashboard-more {
grid-column: 1 / -1;
}
}
@media (max-width: 900px) {
.ux-shell-ready .sidebar {
width: min(86vw, 300px);
}
.ux-shell-ready .main-wrapper {
margin-left: 0;
}
.ux-shell-ready main {
padding: 20px 18px 38px;
}
.ux-shell-ready .topbar {
padding: 10px 18px;
}
.ux-shell-ready .command-search {
min-width: 0;
}
.ux-command-center {
grid-template-columns: 1fr;
}
.ux-command-actions {
justify-content: flex-start;
}
}
@media (max-width: 680px) {
.ux-shell-ready .topbar-context-icon,
.ux-shell-ready .topbar-kicker,
.ux-shell-ready .command-search span,
.ux-shell-ready .command-search kbd {
display: none;
}
.ux-shell-ready .command-search {
justify-content: center;
min-width: 40px;
padding-inline: 10px;
width: 40px;
}
.ux-shell-ready .dashboard-kpis,
.ux-shell-ready .dashboard-rail {
grid-template-columns: 1fr;
}
.ux-command-actions {
display: grid;
grid-template-columns: 1fr;
}
.ux-command-action {
justify-content: center;
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
.ux-nav-section > summary::after {
transition: none;
}
}
+336
View File
@@ -0,0 +1,336 @@
(() => {
'use strict';
const NAV_SECTIONS = [
{
id: 'start',
label: 'Start',
tabs: ['dashboard', 'today', 'generator'],
},
{
id: 'commerce',
label: 'Verkaufen',
tabs: ['history', 'quality-center', 'publish', 'inventory'],
},
{
id: 'content',
label: 'Inhalte & AI',
tabs: ['media-library', 'image-factory', 'flux-studio', 'templates'],
},
{
id: 'system',
label: 'Werkzeuge & System',
collapsible: true,
tabs: ['extensions', 'settings', 'admin', 'fees', 'trash'],
},
];
const VIEW_META = {
dashboard: { title: 'Übersicht', kicker: 'Start' },
today: { title: 'Aufgaben', kicker: 'Arbeitsbereich' },
generator: { title: 'Neuer Artikel', kicker: 'Erstellen' },
history: { title: 'Artikel', kicker: 'Verkaufen' },
'quality-center': { title: 'Qualität', kicker: 'Optimieren' },
publish: { title: 'Veröffentlichen', kicker: 'Marktplätze' },
inventory: { title: 'Lager', kicker: 'Bestand' },
templates: { title: 'Vorlagen', kicker: 'Inhalte' },
'flux-studio': { title: 'FLUX Studio', kicker: 'AI-Bilder' },
'media-library': { title: 'Medien', kicker: 'Bilder' },
'image-factory': { title: 'Bildwerkstatt', kicker: 'Produktion' },
extensions: { title: 'Erweiterungen', kicker: 'Browser' },
settings: { title: 'Einstellungen', kicker: 'System' },
admin: { title: 'Verwaltung', kicker: 'Benutzer & Betrieb' },
fees: { title: 'Gebühren', kicker: 'Kalkulation' },
trash: { title: 'Papierkorb', kicker: 'Wiederherstellen' },
};
const NAV_LABELS = {
dashboard: 'Übersicht',
today: 'Aufgaben',
generator: 'Neuer Artikel',
history: 'Artikel',
'quality-center': 'Qualität',
publish: 'Veröffentlichen',
inventory: 'Lager',
templates: 'Vorlagen',
'flux-studio': 'FLUX Studio',
'media-library': 'Medien',
'image-factory': 'Bildwerkstatt',
extensions: 'Erweiterungen',
settings: 'Einstellungen',
admin: 'Verwaltung',
fees: 'Gebühren',
trash: 'Papierkorb',
};
function getNavItem(tab) {
return document.querySelector(`.sidebar-nav .nav-item[data-tab="${tab}"]`);
}
function createSection(section) {
const element = document.createElement(section.collapsible ? 'details' : 'section');
element.className = `ux-nav-section ux-nav-section-${section.id}`;
element.dataset.uxNavSection = section.id;
if (section.collapsible) {
const stored = localStorage.getItem('vendoo_ux_system_nav_open');
element.open = stored === 'true' || section.tabs.some(tab => getNavItem(tab)?.classList.contains('active'));
const summary = document.createElement('summary');
summary.textContent = section.label;
element.append(summary);
element.addEventListener('toggle', () => {
localStorage.setItem('vendoo_ux_system_nav_open', String(element.open));
});
} else {
const label = document.createElement('div');
label.className = 'ux-nav-section-label';
label.textContent = section.label;
element.append(label);
}
const items = document.createElement('div');
items.className = 'ux-nav-section-items';
items.dataset.uxNavItems = section.id;
element.append(items);
return element;
}
function reorganizeNavigation() {
const nav = document.querySelector('.sidebar-nav');
if (!nav || nav.querySelector('.ux-nav-sections')) return;
const itemsByTab = new Map();
nav.querySelectorAll('.nav-item[data-tab]').forEach(item => {
itemsByTab.set(item.dataset.tab, item);
const label = item.querySelector('span');
if (label && NAV_LABELS[item.dataset.tab]) label.textContent = NAV_LABELS[item.dataset.tab];
item.setAttribute('aria-current', item.classList.contains('active') ? 'page' : 'false');
});
const shell = document.createElement('div');
shell.className = 'ux-nav-sections';
for (const section of NAV_SECTIONS) {
const sectionElement = createSection(section);
const container = sectionElement.querySelector('.ux-nav-section-items');
for (const tab of section.tabs) {
const item = itemsByTab.get(tab);
if (item) container.append(item);
}
shell.append(sectionElement);
}
const assigned = new Set(NAV_SECTIONS.flatMap(section => section.tabs));
const remaining = [...itemsByTab.entries()].filter(([tab]) => !assigned.has(tab));
if (remaining.length) {
const other = createSection({ id: 'other', label: 'Weitere', collapsible: true, tabs: [] });
const container = other.querySelector('.ux-nav-section-items');
remaining.forEach(([, item]) => container.append(item));
shell.append(other);
}
nav.querySelectorAll(':scope > .nav-group').forEach(group => group.remove());
nav.append(shell);
}
function activateView(tab) {
const item = getNavItem(tab);
if (!item || item.classList.contains('hidden')) return;
const systemSection = item.closest('details.ux-nav-section');
if (systemSection) systemSection.open = true;
item.click();
}
function createDashboardCommandCenter() {
const dashboard = document.getElementById('dashboard');
const layout = dashboard?.querySelector('.dashboard-layout');
if (!dashboard || !layout || dashboard.querySelector('.ux-command-center')) return;
const panel = document.createElement('section');
panel.className = 'ux-command-center';
panel.setAttribute('aria-label', 'Schnellstart');
panel.innerHTML = `
<div class="ux-command-copy">
<span>Schnellstart</span>
<h3>Was möchtest du als Nächstes erledigen?</h3>
<p>Die wichtigsten Wege sind direkt erreichbar. Detailauswertungen bleiben darunter verfügbar, ohne den Einstieg zu überladen.</p>
</div>
<div class="ux-command-actions">
<button type="button" class="ux-command-action primary" data-ux-target="generator">+ Neuer Artikel</button>
<button type="button" class="ux-command-action" data-ux-target="today">Aufgaben öffnen</button>
<button type="button" class="ux-command-action" data-ux-target="history">Artikel verwalten</button>
<button type="button" class="ux-command-action" data-ux-target="publish">Veröffentlichen</button>
</div>`;
layout.before(panel);
}
function simplifyDashboardRail() {
const rail = document.querySelector('#dashboard .dashboard-rail');
if (!rail || rail.querySelector('.ux-dashboard-more')) return;
const cards = [...rail.children];
const secondaryCards = cards.filter((_, index) => index > 0);
if (!secondaryCards.length) return;
const details = document.createElement('details');
details.className = 'ux-dashboard-more';
const summary = document.createElement('summary');
summary.textContent = 'Aktivität, Hinweise und Systemstatus';
const content = document.createElement('div');
content.className = 'ux-dashboard-more-content';
secondaryCards.forEach(card => content.append(card));
details.append(summary, content);
rail.append(details);
}
function updateContext() {
const active = document.querySelector('.sidebar-nav .nav-item.active[data-tab]');
const tab = active?.dataset.tab || document.querySelector('.tab-content.active')?.id || 'dashboard';
const meta = VIEW_META[tab] || { title: active?.textContent?.trim() || 'Vendoo', kicker: 'Arbeitsbereich' };
const title = document.getElementById('topbar-title');
const kicker = document.getElementById('topbar-kicker');
if (title) title.textContent = meta.title;
if (kicker) kicker.textContent = meta.kicker;
document.querySelectorAll('.sidebar-nav .nav-item[data-tab]').forEach(item => {
item.setAttribute('aria-current', item === active ? 'page' : 'false');
});
const system = active?.closest('details.ux-nav-section');
if (system) system.open = true;
}
function improveSearchPresentation() {
const search = document.getElementById('topbar-search-btn');
const label = search?.querySelector('span');
const shortcut = search?.querySelector('kbd');
if (label) label.textContent = 'Suchen, Seite öffnen oder Aktion starten …';
if (shortcut) shortcut.textContent = /Mac|iPhone|iPad/.test(navigator.platform) ? '⌘ K' : 'Strg K';
}
function loadGeneratorWorkflowAssets() {
if (!document.querySelector('link[data-ux-generator-workflow]')) {
const style = document.createElement('link');
style.rel = 'stylesheet';
style.href = '/generator-workflow.css?v=1.44.0';
style.dataset.uxGeneratorWorkflow = 'style';
document.head.append(style);
}
if (!document.querySelector('script[data-ux-generator-workflow]')) {
const script = document.createElement('script');
script.src = '/generator-workflow.js?v=1.44.0';
script.async = false;
script.dataset.uxGeneratorWorkflow = 'script';
document.body.append(script);
}
}
function loadArticleOverviewAssets() {
if (!document.querySelector('link[data-ux-article-overview]')) {
const style = document.createElement('link');
style.rel = 'stylesheet';
style.href = '/article-overview-ux.css?v=1.44.0';
style.dataset.uxArticleOverview = 'style';
document.head.append(style);
}
if (!document.querySelector('script[data-ux-article-overview]')) {
const script = document.createElement('script');
script.src = '/article-overview-ux.js?v=1.44.0';
script.async = false;
script.dataset.uxArticleOverview = 'script';
document.body.append(script);
}
}
function loadPublishCenterAssets() {
if (!document.querySelector('link[data-ux-publish-center]')) {
const style = document.createElement('link');
style.rel = 'stylesheet';
style.href = '/publish-center-ux.css?v=1.44.0';
style.dataset.uxPublishCenter = 'style';
document.head.append(style);
}
if (!document.querySelector('script[data-ux-publish-center]')) {
const script = document.createElement('script');
script.src = '/publish-center-ux.js?v=1.44.0';
script.async = false;
script.dataset.uxPublishCenter = 'script';
document.body.append(script);
}
}
function loadMediaLibraryAssets() {
if (!document.querySelector('link[data-ux-media-library]')) {
const style = document.createElement('link');
style.rel = 'stylesheet';
style.href = '/media-library-ux.css?v=1.44.0';
style.setAttribute('data-ux-media-library', 'style');
document.head.append(style);
}
if (!document.querySelector('script[data-ux-media-library]')) {
const script = document.createElement('script');
script.src = '/media-library-ux.js?v=1.44.0';
script.async = false;
script.setAttribute('data-ux-media-library', 'script');
document.body.append(script);
}
}
function loadImageFactoryAssets() {
if (!document.querySelector('link[data-ux-image-factory]')) {
const style = document.createElement('link');
style.rel = 'stylesheet';
style.href = '/image-factory-ux.css?v=1.44.0';
style.setAttribute('data-ux-image-factory', 'style');
document.head.append(style);
}
if (!document.querySelector('script[data-ux-image-factory]')) {
const script = document.createElement('script');
script.src = '/image-factory-ux.js?v=1.44.0';
script.async = false;
script.setAttribute('data-ux-image-factory', 'script');
document.body.append(script);
}
}
function bindUxActions() {
document.addEventListener('click', event => {
const target = event.target.closest?.('[data-ux-target]');
if (!target) return;
activateView(target.dataset.uxTarget);
});
}
function observeNavigation() {
const nav = document.querySelector('.sidebar-nav');
if (!nav) return;
const observer = new MutationObserver(mutations => {
if (mutations.some(mutation => mutation.type === 'attributes' && mutation.attributeName === 'class')) {
updateContext();
}
});
nav.querySelectorAll('.nav-item[data-tab]').forEach(item => observer.observe(item, { attributes: true, attributeFilter: ['class'] }));
window.addEventListener('beforeunload', () => observer.disconnect(), { once: true });
}
function initializeUxFoundation() {
reorganizeNavigation();
createDashboardCommandCenter();
simplifyDashboardRail();
improveSearchPresentation();
bindUxActions();
observeNavigation();
updateContext();
loadGeneratorWorkflowAssets();
loadArticleOverviewAssets();
loadPublishCenterAssets();
loadMediaLibraryAssets();
loadImageFactoryAssets();
document.body.classList.add('ux-shell-ready');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeUxFoundation, { once: true });
} else {
initializeUxFoundation();
}
})();
+261 -6
View File
@@ -61,8 +61,8 @@
},
{
"path": ".github/workflows/ci.yml",
"sha256": "c0e80dbafb1d1491b851eb002dc323e52ea89e7e0456dde034f3303a8f4751c2",
"size": 8173
"sha256": "f009390f9672508c3de9e5483bb1acc6da9ea4845a51159e4cc24821882550f4",
"size": 8710
},
{
"path": ".github/workflows/release.yml",
@@ -769,6 +769,16 @@
"sha256": "fca39cfecba10442bc09c0685504bb4552f6569f1768d46737cb7ff6d7907910",
"size": 1465
},
{
"path": "docs/engineering/ux-slice-16-command-palette-registry.md",
"sha256": "2679daaa9e607544c2d7a31f5b1e72e37dfc530e590cad64cea2475bd1e72d1b",
"size": 3307
},
{
"path": "docs/engineering/ux-slice-17-universal-action-registry.md",
"sha256": "bf6fbc8ba9beb2e9f4451682961c941bc865aeb806fc4bfa0b504139d55c6eb4",
"size": 4936
},
{
"path": "docs/FULL_LISTING_EDITOR_2026.md",
"sha256": "f23c643236c84b0e3715620598f358065748b6d090229003e23df8ad9320b54e",
@@ -2041,8 +2051,8 @@
},
{
"path": "package.json",
"sha256": "cd21451f91312c33bc9bd5b808103c3428854587cc8e9b5ba3b2908085043344",
"size": 4759
"sha256": "6931aecd4f018bd6df1f256e667ffd84d5d386837cb7d4da264e4e78d5ea59c5",
"size": 6818
},
{
"path": "platforms/ebay-de.mjs",
@@ -2064,6 +2074,16 @@
"sha256": "5c925e3934a4c9e2f26dc15b3449a249f5ce20ecebbd459b15365a34804824d6",
"size": 1966
},
{
"path": "public/action-registry.css",
"sha256": "f9a70d1d0c91be31fd29388531f9c46e409ec0bc5b680ae802e3488129ba8ec4",
"size": 5785
},
{
"path": "public/action-registry.js",
"sha256": "fdf52acee5987f82e278afdf029848d19c107ba31c09d90803484689a55e4c38",
"size": 27696
},
{
"path": "public/android-chrome-192x192.png",
"sha256": "a2ea3f5f56dba8bd4d23fa54a26fd941567b4febdd1ed1b4e9f5bcc1c9786e53",
@@ -2084,6 +2104,16 @@
"sha256": "e0e7b8ef01c36f0625b6157aa46bd69de05c17be0b054208fb635d6507b52154",
"size": 30122
},
{
"path": "public/article-overview-ux.css",
"sha256": "7b43703cc3f74eacbb4ece7219472394a45d2f8ead22e3dafefe9fbc5318d8d3",
"size": 11110
},
{
"path": "public/article-overview-ux.js",
"sha256": "4bb29ee4c5eeb334f00eed94f40faf32f36cf736f7c8d743b3d2d40ac58c4fda",
"size": 12549
},
{
"path": "public/brand/favicons/android-chrome-192x192.png",
"sha256": "a2ea3f5f56dba8bd4d23fa54a26fd941567b4febdd1ed1b4e9f5bcc1c9786e53",
@@ -2244,6 +2274,16 @@
"sha256": "e83921d2af9930ab27df1abe64374cb9311189bfaaa951068109177443571a07",
"size": 157997
},
{
"path": "public/command-platform.css",
"sha256": "1e9e268f6521506eee15cf0d990e8ec822a7d692ca0f4cc8bc613326b7eacbe4",
"size": 2807
},
{
"path": "public/command-platform.js",
"sha256": "c772ee1f9306df97f594ff5bd887c5424b29711cdfd64a3fd3886f00c67734d9",
"size": 19572
},
{
"path": "public/core/api-client.js",
"sha256": "1711a9387f2d6e75797b77c8daa92b94d7a15e128476e4b8aa9d54ccb0692a0e",
@@ -2266,8 +2306,8 @@
},
{
"path": "public/design-system/theme-runtime.js",
"sha256": "308f1c0e2ed1de34aa11545a1a660aab02631e422d6c6945e3203d11666784fc",
"size": 4004
"sha256": "7ce328987c17873495db28d8da09bbeb3e1e5e6f966a6d948a5a45a05656d41e",
"size": 10940
},
{
"path": "public/design-system/tokens/source.json",
@@ -2284,11 +2324,61 @@
"sha256": "15bf8dd655b34a8015447e834d387af12685238ede598f5f0e7927eea2933483",
"size": 5503
},
{
"path": "public/flux-studio-ux.css",
"sha256": "b8c0f03bb573d0470733871fbccc7ec1af1539d73d492a78601fc9fc1a09f789",
"size": 11968
},
{
"path": "public/flux-studio-ux.js",
"sha256": "b0711042b966dea3e0ddf21a89c8d5f3721a15a6f33d321631da34337247f978",
"size": 19112
},
{
"path": "public/generator-workflow.css",
"sha256": "25278a86017fc23ec5f67ab5d8fd03a8b3f192d050f7841786dbe8e871f50a4c",
"size": 9763
},
{
"path": "public/generator-workflow.js",
"sha256": "4331d0907803de271eace5dbddb8ec0b0b1814181b0bf0d0ae95baed324fbc3b",
"size": 18251
},
{
"path": "public/global-navigation-ux.css",
"sha256": "355842efe1e2c7f64081d24f81bb8f20ffccec21fc49dc42d048e1baa90b8699",
"size": 8138
},
{
"path": "public/global-navigation-ux.js",
"sha256": "38c5e10fe26816b5858db1d68492e0c0b0eec762e07c0cee1166b182d1f9abf2",
"size": 18208
},
{
"path": "public/image-factory-ux.css",
"sha256": "030b3327ac7c657a8c746a91a50d277071e4f89489b71351967b5906011712a7",
"size": 14238
},
{
"path": "public/image-factory-ux.js",
"sha256": "b1d0be4a3dbb55f95ea91f26f248fd87f15d73ee26bcfa2e7d9a6de3c6d3291b",
"size": 15944
},
{
"path": "public/index.html",
"sha256": "2af933209c3830cdd4d692185aba6b2b23f9c17370dcb5a785e1f7fbafe3fee6",
"size": 180904
},
{
"path": "public/inventory-workspace.css",
"sha256": "d636739c3061e10bc77ba89fd62346c7438638223d8d9890c01eba70245787ec",
"size": 14787
},
{
"path": "public/inventory-workspace.js",
"sha256": "b77144992accc148ea7591b4505a22ec917b6e7721dac57e60dda598582c932c",
"size": 17493
},
{
"path": "public/login.html",
"sha256": "ebd37a3757dee6bf7cbe5550080fb184b043564d174c446b49226f1e88638f7c",
@@ -2299,6 +2389,16 @@
"sha256": "e37558ee9612a36db8a19999fbb320f1c249a8e155f94c83a82926bec920f989",
"size": 6240
},
{
"path": "public/media-library-ux.css",
"sha256": "449d8970b6ece367b413475ddd1de2a97d852e7af007f90504c29197aaa92112",
"size": 14934
},
{
"path": "public/media-library-ux.js",
"sha256": "484311ce5b04eec3654a5ee935430cdc2f146d783e24281f920160dcbf57cbee",
"size": 16184
},
{
"path": "public/mobile-upload-expired.html",
"sha256": "59291f75eca405e3856bafea76cc601d681060eb6b36bf82a4be3c3ab7eac290",
@@ -2319,6 +2419,16 @@
"sha256": "f13846801a8493aa43df6ec3d78b826b21b59e8f21d47735f68fd0b278e440c7",
"size": 6828
},
{
"path": "public/module-extension-workspace.css",
"sha256": "fb655939db8f1ec045d72d432a02495c2914f1f585f386db88acb8c2bac2d52d",
"size": 5149
},
{
"path": "public/module-extension-workspace.js",
"sha256": "48893c875d27cc8595bc9f2904b3d9da91c08a08a1b5aaf74ec0721abd9f4795",
"size": 11401
},
{
"path": "public/modules/module-manager/module-manager.css",
"sha256": "3132436d52caba719a3460c29a807e6f0703cb2118a80c9fae6c30360498d249",
@@ -2344,11 +2454,71 @@
"sha256": "0edc61f0c2bb604c3adc40813b18fe5ca8d9b846ec2a3d9f406ae26ae313d3ee",
"size": 16304
},
{
"path": "public/publish-center-ux.css",
"sha256": "1b4b93fe87aa8934339f2fb99c3af086af07e828fc1e0840e81ecaf9e01e4319",
"size": 12546
},
{
"path": "public/publish-center-ux.js",
"sha256": "728466f405c65f19160f2d94ad9616e16245010fd2800c97333c0d1ce1c621e0",
"size": 15443
},
{
"path": "public/quality-center-ux.css",
"sha256": "f7d40f1ce9ee9a0595e96be216e7abef3e1417740c7771703a3c8285716a2525",
"size": 10996
},
{
"path": "public/quality-center-ux.js",
"sha256": "e518e26410522a8afa82472f4766112f2811d593b97af1da4578ff5c8a928464",
"size": 14829
},
{
"path": "public/settings-control-center.css",
"sha256": "de14abe9f37584652b686c9a318201c43a896b5512b106224f6bb875aa74ce31",
"size": 5825
},
{
"path": "public/settings-control-center.js",
"sha256": "39f28814137cd84b0dd6e1e5e78fbc48d83dfea9a65f2e6861f267cfbe74b17b",
"size": 9447
},
{
"path": "public/style.css",
"sha256": "f902e26fac4ea4a906cda4834749386153c1eac5be63f329c0eb72adf3dc8622",
"size": 364864
},
{
"path": "public/template-workspace.css",
"sha256": "612291e213c925060ccea8f0884b69ddfa64f39bd358ee67f43642f75e58a7f6",
"size": 11887
},
{
"path": "public/template-workspace.js",
"sha256": "53e8b8220432a269394a0eecef0d62ae4f936b47b747ddea240c9c76490f19e5",
"size": 15112
},
{
"path": "public/today-workspace.css",
"sha256": "5847f2087ed69b4cfaae9dbdb0e7ac84831005603e5d088ef5f9ea813cef0bb2",
"size": 8835
},
{
"path": "public/today-workspace.js",
"sha256": "116fb7b890c93b38ee570b6ee22ef864655e7d186aecf5c091dc0e7c5676cd16",
"size": 13197
},
{
"path": "public/ux-foundation.css",
"sha256": "fbfee7d98bb9460b5cd4e905e826bb26e5f2261f9451fd9f17be78bfb08baa84",
"size": 6652
},
{
"path": "public/ux-foundation.js",
"sha256": "fb21d07e3ee62fa9ae4dc121b9fd2e3f5ffdd4c1a4aeb02245f0166c8e3c3745",
"size": 13043
},
{
"path": "public/vendoo-icons.js",
"sha256": "ff525e0af963e6f54636cfdc1bd2d045aaf82e7b0199755eb5cdf4c52110be2e",
@@ -2529,6 +2699,11 @@
"sha256": "d5320924be6e18dfd656fdba717789e15e6aedbd8c5afbeaffc21e141272edae",
"size": 9982
},
{
"path": "tools/verify-article-overview-ux-1.44.0.mjs",
"sha256": "f063014f4b5c60f85f358d0e2f15761b5c738d88525fe35bac9485b492d41ff9",
"size": 3238
},
{
"path": "tools/verify-capability-graph-1.40.2.mjs",
"sha256": "e3d8c6e8dfd566acbe037ad744edbf60816152d4994698c2a9c46d2b890e246a",
@@ -2599,6 +2774,16 @@
"sha256": "fb8ee4afa1d6abc09106ab032793da8dfef43bcb8614c7fe04086eb52cfb2a0b",
"size": 4161
},
{
"path": "tools/verify-command-palette-registry-1.44.0.mjs",
"sha256": "8093006fdcf6dd6f5c87ba0e2eb228c768015adaa21d3bca1e0fb1755d415152",
"size": 8222
},
{
"path": "tools/verify-command-platform-ux-1.44.0.mjs",
"sha256": "b31a1074fcfbf3267560d3cb155742bf0ab42bd9a9772d9cb83c7c894abedad5",
"size": 2652
},
{
"path": "tools/verify-config-store.mjs",
"sha256": "8f9ede5f112ee03323db3a6c73aee00b7037f5139dad241b11baf5f0e9cee4e0",
@@ -2749,6 +2934,16 @@
"sha256": "677d4a7bcd31d800833d529e815794de6a65f543dd6f06eea2039facd469dce2",
"size": 2180
},
{
"path": "tools/verify-flux-studio-ux-1.44.0.mjs",
"sha256": "5d75fe8212b39deb4456e6dd4f5628b26956f94e725dfec6cc6ee0f1dd9b8aed",
"size": 3837
},
{
"path": "tools/verify-generator-workflow-1.44.0.mjs",
"sha256": "4653cf2a4c6f405b32059a7d61a6ba02c8655f348d725cd87c1affe94e92a7e2",
"size": 3511
},
{
"path": "tools/verify-git-ignore-boundaries-1.41.2.mjs",
"sha256": "e06cda8d9850a84cde34a99009c51a18dbc1b7a66e45c82f35aadd631a71540b",
@@ -2769,6 +2964,11 @@
"sha256": "ad2d1df5725da388ca427ddf6da0e0d62bf02999c6554e8c1ab918d63f5b8d50",
"size": 3724
},
{
"path": "tools/verify-global-navigation-ux-1.44.0.mjs",
"sha256": "f1ff9fbfeca5e6c0d53689c62eaae0af23e611f8e236675ab047f7b0e2d50321",
"size": 2723
},
{
"path": "tools/verify-hotfix-1.23.1.mjs",
"sha256": "c21d4f4544c0d6a7f0c90b4c38d4ceb2276b3ca7e4825433cc503bbbfccb2c82",
@@ -2829,6 +3029,11 @@
"sha256": "4df609fa104d69bc00995b7ede6251fe7c64970df1f7c99d36a6dcb94ed65eaa",
"size": 4026
},
{
"path": "tools/verify-image-factory-ux-1.44.0.mjs",
"sha256": "a9258a548c9a1b66a4cb7219a59fa8a71cbbc0e3271e17662abbb17aba32fad2",
"size": 3535
},
{
"path": "tools/verify-initial-disabled-modules-1.41.0.mjs",
"sha256": "9492911fac3771e3b906b5a583e4bbf136ec8383b5fc23b0478f58b7407f7c65",
@@ -2859,6 +3064,21 @@
"sha256": "f7267131d3c7d7e373ff48ec66ab072ec2f74d66aa02cbf4e71209bf90fca750",
"size": 4693
},
{
"path": "tools/verify-inventory-workspace-ux-1.44.0.mjs",
"sha256": "0d798f714b5f7cd265c922c078ac646eb85c58561fe99ff89b449f6b7b2658dd",
"size": 4063
},
{
"path": "tools/verify-media-library-ux-1.44.0.mjs",
"sha256": "db892421265e2c0f82581db13013afc17fcc0b2ebf9b38d7c0006f236aeae716",
"size": 3326
},
{
"path": "tools/verify-module-extension-workspace-ux-1.44.0.mjs",
"sha256": "5792773a4114a6be151bf9cef24ec928694a4312225ed41bb42ae23437db58e6",
"size": 3269
},
{
"path": "tools/verify-module-manager-1.40.0.mjs",
"sha256": "33ca957bf8acd3cc73354ef4c8d5e4b61c44810ecd8a8994ff89305cf1b9429c",
@@ -3024,6 +3244,16 @@
"sha256": "46f1b5a658f6b406f427686ca0961ee18bb18b18d0ca3e219bfbedcc9b1522f0",
"size": 11063
},
{
"path": "tools/verify-publish-center-ux-1.44.0.mjs",
"sha256": "2bb1f3ba2e7c3d6312750c6165c808b67c9d444df749f30a2d176b0570e68085",
"size": 2738
},
{
"path": "tools/verify-quality-center-ux-1.44.0.mjs",
"sha256": "771645a4c9454420f62f3d08f78103591bdedaadc9d2fd0b31d4b9d846a0eae9",
"size": 4265
},
{
"path": "tools/verify-release-1.24.0.mjs",
"sha256": "551ec082e47dbb54ff8c942985face7dead33b684c9e50218ba31fe20ef7d8d4",
@@ -3124,6 +3354,11 @@
"sha256": "3b3e52ef41fee7f5d96a2a96f9b0ecdc51a2fa00837fa466384b340aba3e6653",
"size": 5491
},
{
"path": "tools/verify-settings-control-center-ux-1.44.0.mjs",
"sha256": "884e6187ba8d0ae4dd2fdf19dbc28cc3898062863d4c0fcd26f049b300239e5d",
"size": 4034
},
{
"path": "tools/verify-slice30-mock.mjs",
"sha256": "31a120622ce8bb8451014f2662366f81abd28d0f3be59adfe35d17e0ada1838f",
@@ -3159,6 +3394,11 @@
"sha256": "e7a02bef89e810b1c704f68b2177478da5e41457364209ac369589de992a9e98",
"size": 3402
},
{
"path": "tools/verify-template-workspace-ux-1.44.0.mjs",
"sha256": "774e72e10a36cb4a4b86a5ad9d58bebb1fa220f9988d54fa1a02f9365c50fc1c",
"size": 4148
},
{
"path": "tools/verify-theme-manager-1.36.0.mjs",
"sha256": "c9aa6693aa93ff1f220a2ce3bc35ef714a0b18a53eae0f907879fc92c66c1e38",
@@ -3199,11 +3439,21 @@
"sha256": "c721fda8a7a5b569a1c4beb05255261f51fbb8935d670cc0092fef30ffd1fe4a",
"size": 6130
},
{
"path": "tools/verify-today-workspace-ux-1.44.0.mjs",
"sha256": "963629002cad3a4e878088c07296dfad64bac593335e635753da53ef55f5bff4",
"size": 4123
},
{
"path": "tools/verify-ui-contracts-1.26.1.mjs",
"sha256": "0f3f684019763a14f5fbaec42e42c680cf748d930d46cd607fc61eb413ca4553",
"size": 2360
},
{
"path": "tools/verify-universal-action-registry-1.44.0.mjs",
"sha256": "c7fff136c46baf894dca98dd63fd4415af3a2842955c8f7226c72513f96292e8",
"size": 9980
},
{
"path": "tools/verify-updater-ci-lifecycle-3.4.mjs",
"sha256": "02a0e0d28bfcc3424fe6929ebbe79b0e8000e35d8d4d60aa8b83622baa6800d1",
@@ -3224,6 +3474,11 @@
"sha256": "ae015616e289a4bfb371a2823c35e8e5a013cbdefd4005c47ac972c2e5df7516",
"size": 46
},
{
"path": "tools/verify-ux-foundation-1.44.0.mjs",
"sha256": "08ce61bf0befad1cc4203ad506b31af393a0484f3f9333f5cf1eea97de403af3",
"size": 2954
},
{
"path": "tools/verify-windows-sqlite-cleanup-regression-1.43.0.mjs",
"sha256": "fd900efee640685ee1a0deb515513ba305b4a6e6686dc68e485f7ea26b41ca95",
+47
View File
@@ -0,0 +1,47 @@
import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
const root = process.cwd();
const outputDir = path.join(root, 'artifacts');
const outputFile = path.join(outputDir, 'ux-platform-gates.log');
const node = process.execPath;
const gates = [
['Navigation API', 'tools/verify-navigation-api-1.44.0.mjs'],
['Safe Action Adapters', 'tools/verify-safe-action-adapters-1.44.0.mjs'],
];
fs.mkdirSync(outputDir, { recursive: true });
const lines = [
'Vendoo UX Platform Gates',
`Platform: ${process.platform}`,
`Node: ${process.version}`,
`Working directory: ${root}`,
'',
];
let failed = false;
for (const [name, relativeScript] of gates) {
const script = path.join(root, relativeScript);
lines.push(`=== ${name} ===`);
const result = spawnSync(node, [script], {
cwd: root,
encoding: 'utf8',
env: process.env,
windowsHide: true,
});
if (result.stdout) lines.push(result.stdout.trimEnd());
if (result.stderr) lines.push(result.stderr.trimEnd());
const exitCode = Number.isInteger(result.status) ? result.status : 1;
lines.push(`Exit code: ${exitCode}`, '');
if (exitCode !== 0 || result.error) {
failed = true;
if (result.error) lines.push(`Runner error: ${result.error.message}`, '');
}
}
lines.push(failed ? 'RESULT: FAILED' : 'RESULT: PASSED');
const report = `${lines.join('\n')}\n`;
fs.writeFileSync(outputFile, report, 'utf8');
process.stdout.write(report);
process.exitCode = failed ? 1 : 0;
@@ -0,0 +1,56 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const root = resolve(process.cwd());
const read = path => readFileSync(resolve(root, path), 'utf8');
const loader = read('public/ux-foundation.js');
const css = read('public/article-overview-ux.css');
const js = read('public/article-overview-ux.js');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const assert = (condition, message) => { if (!condition) failures.push(message); };
assert(loader.includes('/article-overview-ux.css?v=1.44.0'), 'Article-Overview-Stylesheet wird nicht über die UX-Foundation geladen.');
assert(loader.includes('/article-overview-ux.js?v=1.44.0'), 'Article-Overview-Script wird nicht über die UX-Foundation geladen.');
assert(pkg.scripts?.['verify:article-overview'] === 'node tools/verify-article-overview-ux-1.44.0.mjs', 'npm-Script verify:article-overview fehlt oder ist falsch.');
assert(String(pkg.scripts?.['verify:all'] || '').includes('verify:article-overview'), 'verify:all enthält das Article-Overview-Gate nicht.');
for (const value of ['', 'active', 'reserved', 'sold']) {
assert(js.includes(`value: '${value}'`), `Status-Schnellfilter fehlt: ${value || 'Alle'}`);
}
for (const id of ['history-list', 'filter-status', 'search-input', 'history-batch-actions', 'history-total-info']) {
assert(js.includes(id), `Bestehender Artikelvertrag wird nicht referenziert: ${id}`);
}
for (const token of ['data-article-view-choice', 'data-article-status', 'data-ux-label', 'MutationObserver', 'localStorage', 'requestAnimationFrame']) {
assert(js.includes(token), `Article-Overview-Vertrag fehlt: ${token}`);
}
assert(js.includes("row.tabIndex = 0"), 'Artikelkarten sind nicht tastaturfokussierbar.');
assert(js.includes("event.key === 'Enter'"), 'Artikelkarten unterstützen Enter nicht.');
assert(!js.includes('onclick='), 'Inline-Eventhandler sind im Article-Overview-Script unzulässig.');
assert(!js.includes('innerHTML +=') && !js.includes('outerHTML'), 'Unsichere DOM-Erweiterung im Article-Overview-Script erkannt.');
for (const selector of [
'.ux-article-toolbar',
'.ux-status-filters',
'.ux-active-filters',
'.ux-batch-more',
'#history[data-article-view="cards"] .history-list',
'#history[data-article-view="list"] .history-table-head',
'@media (max-width: 760px)',
'@media (prefers-reduced-motion: reduce)',
]) {
assert(css.includes(selector), `Article-Overview-CSS-Vertrag fehlt: ${selector}`);
}
assert(css.includes('focus-visible'), 'Sichtbare Tastaturfokusse fehlen im Article-Overview-CSS.');
assert(css.includes('var(--vd-'), 'Article-Overview-CSS verwendet das zentrale Design-Token-System nicht.');
assert(css.includes('repeat(auto-fill, minmax('), 'Adaptive Kartenraster fehlen.');
assert(css.includes('.history-batch-bar:not(.has-selection)'), 'Inaktive Mehrfachaktionen werden nicht reduziert.');
if (failures.length) {
console.error('[ARTICLE-OVERVIEW-UX] Prüfung fehlgeschlagen:');
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('[ARTICLE-OVERVIEW-UX] Karten-/Listenansicht, Status-Schnellfilter, aktive Filter, reduzierte Mehrfachaktionen, Responsive-Vertrag und Tastaturführung sind vollständig.');
@@ -0,0 +1,174 @@
import fs from 'node:fs';
import path from 'node:path';
import vm from 'node:vm';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const commandJs = read('public/command-platform.js');
const navigationJs = read('public/global-navigation-ux.js');
const navigationCss = read('public/global-navigation-ux.css');
const loader = read('public/module-extension-workspace.js');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => { if (!condition) failures.push(message); };
const includesAll = (source, values, label) => values.forEach(value => expect(source.includes(value), `${label}: ${value}`));
includesAll(commandJs, [
'window.VendooCommands',
'aliases',
'visible',
'enabled',
'disabledReason',
'evaluatePermission',
'resolveCommand',
'scoreCommand',
'scoreField',
'favoriteIndex',
'recentIndex',
'includeDisabled',
'includeHidden',
'command-blocked',
'canExecute',
], 'Command Registry Zustands- und Ranking-Vertrag fehlt');
includesAll(navigationJs, [
'getRegistry',
'window.VendooCommands',
'registry.list',
'registry.execute',
'registry.subscribe',
"source: 'command-palette'",
'aria-activedescendant',
'aria-disabled',
'aria-live',
'previousFocus',
'restoreFocus',
'isEditableTarget',
"event.key !== 'Tab'",
"event.key === 'Home'",
"event.key === 'End'",
'Favoriten',
'Zuletzt geöffnet',
'Nicht verfügbar',
], 'Registry-basierte Command Palette unvollständig');
includesAll(navigationCss, [
'.ux-global-search-status',
'.ux-global-search-section',
'.ux-global-search-result-icon',
'.ux-global-search-badge',
'.ux-global-search-result.is-disabled',
'[aria-disabled="true"]',
'@media(max-width:620px)',
'@media(prefers-reduced-motion:reduce)',
':focus-visible',
'var(--',
], 'Command Palette CSS-Vertrag fehlt');
includesAll(loader, [
'loadCommandPlatformAssets',
'loadGlobalNavigationAssets',
'loadCommandPlatformAssets();',
'loadGlobalNavigationAssets();',
], 'Asset-Ladereihenfolge fehlt');
expect(loader.indexOf('loadCommandPlatformAssets();') < loader.indexOf('loadGlobalNavigationAssets();'), 'Command Registry muss vor der Palette geladen werden');
expect(!navigationJs.includes('VIEW_LABELS'), 'Command Palette darf keine zweite statische View-Metadatenquelle führen');
expect(!navigationJs.includes('collectCommands'), 'Command Palette darf Commands nicht aus einer parallelen Liste sammeln');
expect(!/activateView\s*\(\s*command\.view/.test(navigationJs), 'Palette darf Registry-Ausführung nicht durch direkte Navigation umgehen');
expect(!/\.innerHTML\s*=/.test(commandJs + navigationJs), 'Command Platform und Palette dürfen innerHTML nicht setzen');
expect(!/\bfetch\s*\(/.test(commandJs + navigationJs), 'Command Platform und Palette dürfen keine Netzwerkaufrufe ausführen');
expect(!/\b(?:POST|PUT|PATCH|DELETE)\b/.test(commandJs + navigationJs), 'Command Platform und Palette dürfen keine HTTP-Schreiboperationen implementieren');
expect(!/confirm\s*\(/.test(commandJs + navigationJs), 'Command Palette darf keine kritischen Aktionen bestätigen');
expect(!/setInterval\s*\(/.test(commandJs + navigationJs), 'Command Platform und Palette dürfen kein Polling starten');
expect(!/Vendoo(?:ModuleManager|App|Api)\.(?:publish|delete|remove|install|enable|disable)/i.test(navigationJs), 'Palette darf produktive Aktionen nicht direkt ausführen');
function verifyRegistryRuntime() {
const storage = new Map();
const events = [];
let activeView = 'dashboard';
const navItems = new Map();
const makeNavItem = view => ({
dataset: { tab: view },
classList: { contains: name => name === 'active' ? activeView === view : false },
getAttribute: () => 'false',
closest: () => ({ setAttribute() {} }),
click: () => { activeView = view; },
});
for (const view of ['dashboard', 'today', 'generator', 'history', 'quality-center', 'publish', 'inventory', 'templates', 'media-library', 'image-factory', 'flux-studio', 'extensions', 'settings', 'admin', 'fees', 'trash']) navItems.set(view, makeNavItem(view));
const windowObject = {
dispatchEvent(event) { events.push(event); return true; },
addEventListener() {},
VendooPermissions: undefined,
};
const documentObject = {
documentElement: { dataset: {} },
querySelector(selector) {
if (selector === '.sidebar-nav .nav-item.active[data-tab]') return navItems.get(activeView);
if (selector === '.tab-content.active' || selector === '.sidebar-nav') return null;
const match = selector.match(/^\.sidebar-nav \.nav-item\[data-tab="(.+)"\]$/);
return match ? navItems.get(match[1]) || null : null;
},
getElementById() { return null; },
createElement() { throw new Error('DOM-Erzeugung ist im Registry-Runtime-Test nicht erwartet.'); },
addEventListener() {},
};
const context = vm.createContext({
window: windowObject,
document: documentObject,
localStorage: {
getItem(key) { return storage.get(key) ?? null; },
setItem(key, value) { storage.set(key, String(value)); },
},
CustomEvent: class CustomEvent { constructor(type, init = {}) { this.type = type; this.detail = init.detail; } },
MutationObserver: class MutationObserver { observe() {} disconnect() {} },
CSS: { escape: value => String(value) },
history: { back() {}, forward() {} },
console,
});
vm.runInContext(commandJs, context, { filename: 'public/command-platform.js' });
const commands = windowObject.VendooCommands;
expect(commands && typeof commands.list === 'function', 'Runtime: VendooCommands.list fehlt');
expect(commands?.list({ query: 'home' })[0]?.id === 'view:dashboard', 'Runtime: Aliassuche findet Dashboard nicht');
commands.register({
id: 'test:disabled',
title: 'Spezialprüfung',
description: 'Prüft einen externen Dienst',
aliases: ['Schnelltest'],
keywords: ['Diagnose'],
enabled: false,
disabledReason: 'Dienst nicht verbunden.',
execute: () => true,
});
const disabled = commands.list({ query: 'Schnelltest' })[0];
expect(disabled?.id === 'test:disabled', 'Runtime: Aliasranking liefert falschen Command');
expect(disabled?.enabled === false && disabled?.disabledReason === 'Dienst nicht verbunden.', 'Runtime: deaktivierter Zustand oder Begründung fehlt');
expect(commands.execute('test:disabled') === false, 'Runtime: deaktivierter Command wurde ausgeführt');
expect(events.some(event => event.type === 'vendoo:command-blocked' && event.detail?.commandId === 'test:disabled'), 'Runtime: command-blocked Event fehlt');
commands.register({ id: 'test:permission', title: 'Geheimer Bereich', permission: 'admin', execute: () => true });
expect(!commands.list({ query: 'Geheimer' }).some(command => command.id === 'test:permission'), 'Runtime: Permission ohne Resolver muss deny-by-default sein');
windowObject.VendooPermissions = { can: permission => permission === 'admin' };
expect(commands.list({ query: 'Geheimer' }).some(command => command.id === 'test:permission'), 'Runtime: Permission-Resolver wird nicht ausgewertet');
commands.toggleFavorite('test:disabled');
expect(commands.list()[0]?.id === 'test:disabled', 'Runtime: Favorit wird ohne Suchbegriff nicht priorisiert');
expect(commands.execute('view:history', { source: 'runtime-test' }) === true, 'Runtime: sichtbarer Navigationscommand konnte nicht ausgeführt werden');
expect(windowObject.VendooNavigation.recents[0] === 'history', 'Runtime: erfolgreicher Navigationscommand wird nicht als Recent erfasst');
}
verifyRegistryRuntime();
expect(pkg.scripts?.['verify:command-palette-registry'] === 'node tools/verify-command-palette-registry-1.44.0.mjs', 'verify:command-palette-registry fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:command-palette-registry'), 'verify:all enthält Command-Palette-Registry-Gate nicht');
if (failures.length) {
console.error('Command Palette Registry Gate fehlgeschlagen:');
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Command Palette Registry Gate bestanden.');
@@ -0,0 +1,31 @@
import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const js = read('public/command-platform.js');
const css = read('public/command-platform.css');
const loader = read('public/module-extension-workspace.js');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => { if (!condition) failures.push(message); };
const includesAll = (source, values, label) => values.forEach(value => expect(source.includes(value), `${label}: ${value}`));
includesAll(js, ['uxCommandPlatformReady','VendooCommands','VendooNavigation','register','unregister','execute','toggleFavorite','history.back','history.forward','ux-command-breadcrumb','ux-command-favorite-toggle','ux-command-drawer','vendoo_command_favorites_v1','vendoo_navigation_recents_v1','MutationObserver','CustomEvent'], 'Command Platform JS-Vertrag fehlt');
includesAll(css, ['.ux-command-platform-chrome','.ux-command-breadcrumb','.ux-command-favorite-toggle','.ux-command-drawer','.ux-command-drawer-item','@media(max-width:780px)','@media(max-width:520px)','@media(prefers-reduced-motion:reduce)',':focus-visible','var(--'], 'Command Platform CSS-Vertrag fehlt');
includesAll(loader, ['loadCommandPlatformAssets','command-platform.css?v=1.44.0','command-platform.js?v=1.44.0','data-ux-command-platform','loadCommandPlatformAssets();','loadGlobalNavigationAssets();'], 'Command Platform Loader fehlt');
expect(loader.indexOf('loadCommandPlatformAssets();') < loader.indexOf('loadGlobalNavigationAssets();'), 'Command Platform muss vor globaler Navigation geladen werden');
expect(pkg.scripts?.['verify:command-platform'] === 'node tools/verify-command-platform-ux-1.44.0.mjs', 'verify:command-platform fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:command-platform'), 'verify:all enthält Command-Platform-Gate nicht');
expect(!/\bfetch\s*\(/.test(js), 'Command Platform darf keine Netzwerkaufrufe ausführen');
expect(!/\b(?:POST|PUT|PATCH|DELETE)\b/.test(js), 'Command Platform darf keine HTTP-Schreiboperationen implementieren');
expect(!/\.innerHTML\s*=/.test(js), 'Command Platform darf innerHTML nicht setzen');
expect(!/confirm\s*\(/.test(js), 'Command Platform darf keine kritischen Aktionen bestätigen');
expect(!/setInterval\s*\(/.test(js), 'Command Platform darf kein Polling starten');
if (failures.length) {
console.error('Command Platform UX Gate fehlgeschlagen:');
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Command Platform UX Gate bestanden.');
+102
View File
@@ -0,0 +1,102 @@
import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const js = read('public/flux-studio-ux.js');
const css = read('public/flux-studio-ux.css');
const loader = read('public/design-system/theme-runtime.js');
const html = read('public/index.html');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => {
if (!condition) failures.push(message);
};
const includesAll = (source, values, label) => {
for (const value of values) expect(source.includes(value), `${label}: ${value}`);
};
includesAll(html, [
'id="flux-studio"',
'id="flux-studio-prompt"',
'id="flux-studio-generate"',
'id="flux-studio-preview-image"',
'id="flux-studio-engine-chip"',
'id="flux-runtime-summary"',
'id="flux-job-center-body"',
'id="flux-job-filter"',
'id="flux-job-list"',
'id="flux-studio-history"',
'id="flux-studio-use-in-generator"',
], 'Bestehender FLUX-Studio-Vertrag fehlt');
includesAll(loader, [
'flux-studio-ux.css?v=1.44.0',
'flux-studio-ux.js?v=1.44.0',
'data-ux-flux-studio',
'loadFluxStudioAssets()',
], 'UX-Runtime bindet FLUX Studio nicht vollständig ein');
expect(pkg.scripts?.['verify:flux-studio-ux'] === 'node tools/verify-flux-studio-ux-1.44.0.mjs', 'verify:flux-studio-ux Script fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:flux-studio-ux'), 'verify:all enthält das FLUX-Studio-Gate nicht');
includesAll(js, [
'ux-flux-view-navigation',
'uxFluxView',
'ux-flux-workflow',
'uxFluxStep',
'ux-flux-system-panel',
'ux-flux-advanced',
'ux-flux-preview-primary',
'ux-flux-job-quick-filters',
'uxFluxJobFilter',
'flux-studio-prompt',
'flux-studio-generate',
'flux-studio-preview-image',
'flux-job-center-body',
'flux-studio-history',
'MutationObserver',
'requestAnimationFrame',
'scrollIntoView',
'aria-selected',
'ArrowLeft',
'ArrowRight',
], 'FLUX-Studio-UX-Vertrag fehlt');
includesAll(css, [
'#flux-studio.ux-flux-studio-ready',
'.ux-flux-view-navigation',
'.ux-flux-workflow',
'.ux-flux-steps',
'.ux-flux-system-panel',
'.ux-flux-advanced',
'.ux-flux-preview-primary',
'.ux-flux-job-quick-filters',
'.ux-flux-panel-hidden',
'@media (max-width: 760px)',
'@media (max-width: 470px)',
'@media (prefers-reduced-motion: reduce)',
':focus-visible',
'var(--',
], 'FLUX-Studio-CSS-Vertrag fehlt');
expect(!/\bfetch\s*\(/.test(js), 'Additive FLUX-Studio-UX darf keine eigenen Netzwerkaufrufe ausführen');
expect(!/\bapi\s*\(/.test(js), 'Additive FLUX-Studio-UX darf die Vendoo-API nicht direkt aufrufen');
expect(!/\.innerHTML\s*=/.test(js), 'FLUX-Studio-UX darf keine innerHTML-Zuweisungen verwenden');
expect(!/\.outerHTML\s*=/.test(js), 'FLUX-Studio-UX darf keine outerHTML-Zuweisungen verwenden');
expect(!/\.onclick\s*=/.test(js), 'FLUX-Studio-UX darf keine onclick-Zuweisungen verwenden');
expect(!/confirm\s*\(/.test(js), 'Gefährliche FLUX-Aktionen müssen in der bestehenden geprüften Logik bleiben');
expect(!/\b(?:DELETE|PATCH|POST|PUT)\b/.test(js), 'FLUX-Studio-UX darf keine Schreiboperationen implementieren');
expect(!/runFluxStudioGeneration\s*\(/.test(js), 'UX-Schicht darf keine FLUX-Produktion direkt auslösen');
expect(!/deleteFluxStudioImage\s*\(/.test(js), 'UX-Schicht darf keine FLUX-Bilder direkt löschen');
expect(!/toggleFluxFavorite\s*\(/.test(js), 'UX-Schicht darf Favoritenstatus nicht direkt verändern');
expect(!/state\.flux/.test(js), 'Additive UX darf den produktiven FLUX-State nicht direkt verändern');
if (failures.length) {
console.error('FLUX Studio UX Gate fehlgeschlagen:');
for (const failure of failures) console.error(`- ${failure}`);
process.exit(1);
}
console.log('FLUX Studio UX Gate bestanden.');
@@ -0,0 +1,65 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const root = resolve(process.cwd());
const read = path => readFileSync(resolve(root, path), 'utf8');
const ux = read('public/ux-foundation.js');
const js = read('public/generator-workflow.js');
const css = read('public/generator-workflow.css');
const index = read('public/index.html');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const assert = (condition, message) => { if (!condition) failures.push(message); };
assert(ux.includes('generator-workflow.css'), 'Generator-Workflow-CSS wird nicht durch die UX Foundation geladen.');
assert(ux.includes('generator-workflow.js'), 'Generator-Workflow-JavaScript wird nicht durch die UX Foundation geladen.');
assert(pkg.scripts?.['verify:generator-workflow'] === 'node tools/verify-generator-workflow-1.44.0.mjs', 'npm-Script verify:generator-workflow fehlt oder ist falsch.');
assert(String(pkg.scripts?.['verify:all'] || '').includes('verify:generator-workflow'), 'verify:all enthält das Generator-Workflow-Gate nicht.');
for (const id of [
'generator', 'generator-commandbar', 'generate-btn', 'save-btn', 'dropzone', 'photo-preview',
'platform-buttons', 'provider-buttons', 'template-select', 'result-section', 'result-title',
]) {
assert(index.includes(`id="${id}"`), `Bestehender Generator-Vertrag fehlt: #${id}`);
}
for (const marker of [
'ux-generator-flow', 'data-ux-generator-stage', 'data-ux-generator-action',
'createAdvancedSettings', 'simplifyMediaPanel', 'simplifyResultPanel',
'MutationObserver', 'requestAnimationFrame', 'aria-current', 'focusStage',
]) {
assert(js.includes(marker), `Generator-Workflow-Marker fehlt: ${marker}`);
}
for (const stage of ['photos', 'settings', 'result']) {
assert(js.includes(`data-ux-generator-stage="${stage}"`), `Geführter Schritt fehlt: ${stage}`);
assert(js.includes(`${stage}:`), `Schrittziel fehlt: ${stage}`);
}
assert(js.includes('localStorage'), 'Einklappzustände werden nicht benutzerspezifisch gespeichert.');
assert(js.includes("generate.click()"), 'Geführte Generieren-Aktion ist nicht an den bestehenden Button delegiert.');
assert(!js.includes('onclick='), 'Inline-Eventhandler sind im Generator-Workflow unzulässig.');
assert(!js.includes('fetch('), 'Der UX-Workflow darf keine neue API- oder Datenlogik einführen.');
assert(!js.includes('innerHTML +='), 'Unsichere inkrementelle HTML-Erweiterung erkannt.');
for (const selector of [
'.ux-generator-flow', '.ux-generator-stages', '.ux-generator-advanced',
'.ux-generator-optional', '.ux-generator-panel-focus',
'@media (max-width: 1100px)', '@media (max-width: 760px)',
'@media (prefers-reduced-motion: reduce)', 'focus-visible',
]) {
assert(css.includes(selector), `Generator-Workflow-CSS-Vertrag fehlt: ${selector}`);
}
assert(css.includes('var(--'), 'Generator-Workflow verwendet das zentrale Token-/Variablensystem nicht.');
assert(css.includes('grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr)'), 'Der reduzierte Zwei-Spalten-Vertrag fehlt.');
assert(css.includes('grid-column: 1 / -1'), 'Die Ergebnisfläche wird nicht über die volle Breite geführt.');
if (failures.length) {
console.error('[GENERATOR-WORKFLOW] Prüfung fehlgeschlagen:');
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('[GENERATOR-WORKFLOW] Geführter Drei-Schritt-Ablauf, reduzierte Einstellungen, responsive Struktur und bestehende Datenverträge sind vollständig.');
@@ -0,0 +1,33 @@
import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const js = read('public/global-navigation-ux.js');
const css = read('public/global-navigation-ux.css');
const loader = read('public/module-extension-workspace.js');
const html = read('public/index.html');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => { if (!condition) failures.push(message); };
const includesAll = (source, values, label) => values.forEach(value => expect(source.includes(value), `${label}: ${value}`));
includesAll(html, ['id="topbar"','id="topbar-search-btn"','class="topbar-context"','class="nav-item active"','data-tab="dashboard"'], 'Bestehender Navigations-DOM-Vertrag fehlt');
includesAll(loader, ['loadGlobalNavigationAssets','global-navigation-ux.css?v=1.44.0','global-navigation-ux.js?v=1.44.0','data-ux-global-navigation'], 'Asset-Loader fehlt');
includesAll(js, ['uxGlobalNavigationReady','metaKey','ctrlKey','history.pushState','history.replaceState','history.back','history.forward','popstate','Alt + Pfeil links','ux-nav-back','ux-nav-forward','ux-global-search','aria-modal','role', 'Escape', 'ArrowDown', 'ArrowUp', 'Enter'], 'Globale Navigation unvollständig');
includesAll(css, ['.ux-nav-history','.ux-nav-history-button','.ux-global-search','.ux-global-search-panel','.ux-global-search-result','@media(max-width:620px)','@media(prefers-reduced-motion:reduce)',':focus-visible','var(--'], 'Globale Navigation CSS unvollständig');
expect(pkg.scripts?.['verify:global-navigation'] === 'node tools/verify-global-navigation-ux-1.44.0.mjs', 'verify:global-navigation fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:global-navigation'), 'verify:all enthält Global-Navigation-Gate nicht');
expect(!/\bfetch\s*\(/.test(js), 'Globale Suche darf keine Netzwerkaufrufe ausführen');
expect(!/\b(?:POST|PUT|PATCH|DELETE)\b/.test(js), 'Globale Suche darf keine Schreiboperationen implementieren');
expect(!/\.innerHTML\s*=/.test(js), 'Globale Suche darf innerHTML nicht setzen');
expect(!/confirm\s*\(/.test(js), 'Globale Suche darf keine kritischen Aktionen bestätigen');
expect(!/Vendoo(?:ModuleManager|App|Api)\.(?:publish|delete|remove|install|enable|disable)/i.test(js), 'Globale Suche darf produktive Aktionen nicht direkt ausführen');
expect(!/setInterval\s*\(/.test(js), 'Globale Navigation darf kein Polling starten');
if (failures.length) {
console.error('Global Navigation UX Gate fehlgeschlagen:');
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Global Navigation UX Gate bestanden.');
+91
View File
@@ -0,0 +1,91 @@
import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const js = read('public/image-factory-ux.js');
const css = read('public/image-factory-ux.css');
const loader = read('public/ux-foundation.js');
const html = read('public/index.html');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => {
if (!condition) failures.push(message);
};
const includesAll = (source, values, label) => {
for (const value of values) expect(source.includes(value), `${label}: ${value}`);
};
includesAll(html, [
'id="image-factory"',
'id="image-factory-source-grid"',
'id="image-factory-profile-grid"',
'id="image-factory-selection-count"',
'id="image-factory-submit"',
'id="image-factory-summary"',
'id="image-factory-jobs"',
'id="image-factory-job-page-info"',
], 'Bestehender Bildproduktionsvertrag fehlt');
includesAll(loader, [
'image-factory-ux.css?v=1.44.0',
'image-factory-ux.js?v=1.44.0',
'data-ux-image-factory',
'loadImageFactoryAssets()',
], 'UX-Loader bindet Bildwerkstatt nicht vollständig ein');
expect(pkg.scripts?.['verify:image-factory'] === 'node tools/verify-image-factory-ux-1.44.0.mjs', 'verify:image-factory Script fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:image-factory'), 'verify:all enthält das Bildwerkstatt-Gate nicht');
includesAll(js, [
'ux-image-factory-workflow',
'uxFactoryStep',
'ux-image-factory-stage',
'ux-image-factory-monitor',
'uxFactoryNext',
'uxFactoryJobFilter',
'stageTarget',
'ux-image-factory-stage-jobs',
'image-factory-selection-count',
'image-factory-source-grid',
'image-factory-profile-grid',
'image-factory-submit',
'image-factory-summary',
'MutationObserver',
'requestAnimationFrame',
'scrollIntoView',
], 'Bildwerkstatt-UX-Vertrag fehlt');
includesAll(css, [
'#image-factory.ux-image-factory-ready',
'.ux-image-factory-workflow',
'.ux-image-factory-steps',
'.ux-image-factory-stage',
'.ux-image-factory-monitor',
'.ux-image-factory-job-filter',
'.ux-image-factory-source-controls',
'.image-factory-submit',
'@media (max-width: 760px)',
'@media (max-width: 470px)',
'@media (prefers-reduced-motion: reduce)',
':focus-visible',
'var(--',
], 'Bildwerkstatt-CSS-Vertrag fehlt');
expect(!/\bfetch\s*\(/.test(js), 'Additive Bildwerkstatt-UX darf keine eigenen Netzwerkaufrufe ausführen');
expect(!/\bapi\s*\(/.test(js), 'Additive Bildwerkstatt-UX darf keine Vendoo-API direkt aufrufen');
expect(!/\.innerHTML\s*=/.test(js), 'Bildwerkstatt-UX darf keine innerHTML-Zuweisungen verwenden');
expect(!/\.outerHTML\s*=/.test(js), 'Bildwerkstatt-UX darf keine outerHTML-Zuweisungen verwenden');
expect(!/\.onclick\s*=/.test(js), 'Bildwerkstatt-UX darf keine onclick-Zuweisungen verwenden');
expect(!/confirm\s*\(/.test(js), 'Gefährliche Produktionsaktionen müssen in der bestehenden geprüften Logik bleiben');
expect(!/\b(?:DELETE|PATCH|POST|PUT)\b/.test(js), 'Bildwerkstatt-UX darf keine Schreiboperationen implementieren');
expect(!/imageFactorySelectedSources\s*=|imageFactorySelectedProfiles\s*=|imageFactoryJobs\s*=/.test(js), 'Additive UX darf den produktiven Image-Factory-State nicht überschreiben');
if (failures.length) {
console.error('Image Factory UX Gate fehlgeschlagen:');
for (const failure of failures) console.error(`- ${failure}`);
process.exit(1);
}
console.log('Image Factory UX Gate bestanden.');
@@ -0,0 +1,104 @@
import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const js = read('public/inventory-workspace.js');
const css = read('public/inventory-workspace.css');
const runtime = read('public/design-system/theme-runtime.js');
const html = read('public/index.html');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => {
if (!condition) failures.push(message);
};
const includesAll = (source, values, label) => {
for (const value of values) expect(source.includes(value), `${label}: ${value}`);
};
includesAll(html, [
'id="inventory"',
'class="inventory-header"',
'id="inv-add-location-btn"',
'id="inv-stats"',
'id="inv-locations"',
'id="inv-detail"',
'id="inv-back"',
'id="inv-detail-title"',
'id="inv-rename-btn"',
'id="inv-move-btn"',
'id="inv-detail-list"',
], 'Bestehender Lagerverwaltungsvertrag fehlt');
includesAll(runtime, [
'inventory-workspace.css?v=1.44.0',
'inventory-workspace.js?v=1.44.0',
'data-ux-inventory-workspace',
'loadInventoryWorkspaceAssets()',
], 'Theme-Runtime bindet die Lager-UX nicht vollständig ein');
expect(pkg.scripts?.['verify:inventory-workspace'] === 'node tools/verify-inventory-workspace-ux-1.44.0.mjs', 'verify:inventory-workspace Script fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:inventory-workspace'), 'verify:all enthält das Lager-UX-Gate nicht');
includesAll(js, [
'ux-inventory-overview',
'ux-inventory-metrics',
'uxInventoryMetric',
'data-ux-inventory-next',
'ux-inventory-toolbar',
'data-ux-inventory-search',
'data-ux-inventory-filter',
'data-ux-inventory-sort',
'ux-inventory-needs-attention',
'ux-inventory-detail-toolbar',
'data-ux-inventory-detail-search',
'data-ux-inventory-select-all',
'ux-inventory-selection-bar',
'ux-inventory-more-actions',
'inv-item-check',
'MutationObserver',
'requestAnimationFrame',
'scrollIntoView',
'aria-pressed',
'ArrowLeft',
'ArrowRight',
], 'Lager-UX-Vertrag fehlt');
includesAll(css, [
'#inventory.ux-inventory-ready',
'.ux-inventory-overview',
'.ux-inventory-metrics',
'.ux-inventory-toolbar',
'.ux-inventory-filter-group',
'.ux-inventory-needs-attention',
'.ux-inventory-detail-toolbar',
'.ux-inventory-selection-bar',
'.ux-inventory-more-actions',
'@media (max-width: 700px)',
'@media (max-width: 470px)',
'@media (prefers-reduced-motion: reduce)',
':focus-visible',
'var(--',
], 'Lager-UX-CSS-Vertrag fehlt');
expect(!/\bfetch\s*\(/.test(js), 'Additive Lager-UX darf keine eigenen Netzwerkaufrufe ausführen');
expect(!/\bapi\s*\(/.test(js), 'Additive Lager-UX darf die Vendoo-API nicht direkt aufrufen');
expect(!/\.innerHTML\s*=/.test(js), 'Lager-UX darf keine innerHTML-Zuweisungen verwenden');
expect(!/\.outerHTML\s*=/.test(js), 'Lager-UX darf keine outerHTML-Zuweisungen verwenden');
expect(!/\.onclick\s*=/.test(js), 'Lager-UX darf keine onclick-Zuweisungen verwenden');
expect(!/confirm\s*\(/.test(js), 'Gefährliche Lageraktionen müssen in der bestehenden geprüften Logik bleiben');
expect(!/prompt\s*\(/.test(js), 'Eingaben für Lageränderungen müssen in der bestehenden geprüften Logik bleiben');
expect(!/\b(?:DELETE|PATCH|POST|PUT)\b/.test(js), 'Lager-UX darf keine Schreiboperationen implementieren');
expect(!/bulk-location/.test(js), 'Lager-UX darf die Bulk-Location-API nicht direkt verwenden');
expect(!/loadInventory\s*\(/.test(js), 'Lager-UX darf den produktiven Ladevorgang nicht direkt auslösen');
expect(!/showInventoryDetail\s*\(/.test(js), 'Lager-UX darf die produktive Detailfunktion nicht direkt aufrufen');
expect(!/state\./.test(js), 'Additive Lager-UX darf den produktiven Vendoo-State nicht direkt verändern');
if (failures.length) {
console.error('Inventory Workspace UX Gate fehlgeschlagen:');
for (const failure of failures) console.error(`- ${failure}`);
process.exit(1);
}
console.log('Inventory Workspace UX Gate bestanden.');
+91
View File
@@ -0,0 +1,91 @@
import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const js = read('public/media-library-ux.js');
const css = read('public/media-library-ux.css');
const loader = read('public/ux-foundation.js');
const html = read('public/index.html');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => {
if (!condition) failures.push(message);
};
const includesAll = (source, values, label) => {
for (const value of values) expect(source.includes(value), `${label}: ${value}`);
};
includesAll(html, [
'id="media-library"',
'id="media-grid"',
'id="media-search"',
'id="media-source"',
'id="media-sort"',
'id="media-favorites-only"',
'id="media-selected-count"',
'id="media-lightbox"',
'id="media-lightbox-stage"',
], 'Bestehender Medienvertrag fehlt');
includesAll(loader, [
'media-library-ux.css?v=1.44.0',
'media-library-ux.js?v=1.44.0',
'data-ux-media-library',
'loadMediaLibraryAssets()',
], 'UX-Loader bindet Medienbibliothek nicht vollständig ein');
expect(pkg.scripts?.['verify:media-library'] === 'node tools/verify-media-library-ux-1.44.0.mjs', 'verify:media-library Script fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:media-library'), 'verify:all enthält das Medienbibliothek-Gate nicht');
includesAll(js, [
'DENSITY_KEY',
'data-media-density-option',
'ux-media-filter-details',
'ux-media-active-filters',
'ux-media-filter-chip',
'data-has-media-selection',
'ux-media-batch-more',
'MutationObserver',
'data-media-preview',
'data-media-use',
'data-ux-media-preview-nav',
"event.key === 'ArrowLeft'",
"event.key === 'ArrowRight'",
'requestAnimationFrame',
], 'Medienbibliothek-UX-Vertrag fehlt');
includesAll(css, [
'#media-library[data-media-density="compact"]',
'#media-library[data-media-density="large"]',
'.ux-media-command',
'.ux-media-filter-panel',
'.ux-media-active-filters',
'#media-library[data-has-media-selection] .media-batchbar',
'.ux-media-batch-more',
'#media-library .media-grid',
'#media-library .media-card-actions',
'.ux-media-lightbox-nav',
'@media (max-width: 760px)',
'@media (max-width: 470px)',
'@media (prefers-reduced-motion: reduce)',
':focus-visible',
'var(--',
], 'Medienbibliothek-CSS-Vertrag fehlt');
expect(!/\bfetch\s*\(/.test(js), 'Additive Medien-UX darf keine eigenen Netzwerkaufrufe ausführen');
expect(!/\bapi\s*\(/.test(js), 'Additive Medien-UX darf keine Vendoo-API direkt aufrufen');
expect(!/\.innerHTML\s*=/.test(js), 'Medien-UX darf keine innerHTML-Zuweisungen verwenden');
expect(!/\.outerHTML\s*=/.test(js), 'Medien-UX darf keine outerHTML-Zuweisungen verwenden');
expect(!/\.onclick\s*=/.test(js), 'Medien-UX darf keine onclick-Zuweisungen verwenden');
expect(!/confirm\s*\(/.test(js), 'Gefährliche Medienaktionen müssen in der bestehenden geprüften Logik bleiben');
expect(!/\b(?:DELETE|PATCH|POST|PUT)\b/.test(js), 'Medien-UX darf keine Schreiboperationen implementieren');
if (failures.length) {
console.error('Media Library UX Gate fehlgeschlagen:');
for (const failure of failures) console.error(`- ${failure}`);
process.exit(1);
}
console.log('Media Library UX Gate bestanden.');
@@ -0,0 +1,35 @@
import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const js = read('public/module-extension-workspace.js');
const css = read('public/module-extension-workspace.css');
const runtime = read('public/design-system/theme-runtime.js');
const html = read('public/index.html');
const manager = read('public/modules/module-manager/module-manager.js');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => { if (!condition) failures.push(message); };
const includesAll = (source, values, label) => values.forEach(value => expect(source.includes(value), `${label}: ${value}`));
includesAll(html, ['id="extensions"','id="extensions-grid"','id="extensions-active-browser"','id="extensions-prepare-active"','id="admin-modules-panel"','id="module-manager-summary"','id="module-manager-list"','id="module-manager-search"','id="module-manager-file"','id="module-manager-install"'], 'Bestehender Modul-/Extension-DOM-Vertrag fehlt');
includesAll(manager, ['VendooModuleManager','/api/admin/modules','dataset.moduleAction','module-card','module-dependencies','module-error'], 'Produktiver Modulmanager-Vertrag fehlt');
includesAll(runtime, ['module-extension-workspace.css?v=1.44.0','module-extension-workspace.js?v=1.44.0','data-ux-module-extension-workspace','loadModuleExtensionWorkspaceAssets()'], 'Runtime-Einbindung fehlt');
includesAll(js, ['uxModuleExtensionReady','ux-module-command','ux-module-health','ux-module-filterbar','ux-module-install','ux-extension-board','ux-extension-status','MutationObserver','requestAnimationFrame','localStorage','aria-pressed','module-manager-list','extensions-grid'], 'Workspace-JS-Vertrag fehlt');
includesAll(css, ['.ux-module-command','.ux-module-health','.ux-module-filterbar','.ux-module-install','.ux-extension-board','#module-manager-list','#extensions-grid','@media (max-width:1100px)','@media (max-width:780px)','@media (max-width:520px)','@media (prefers-reduced-motion:reduce)',':focus-visible','var(--'], 'Workspace-CSS-Vertrag fehlt');
expect(pkg.scripts?.['verify:module-extension-workspace'] === 'node tools/verify-module-extension-workspace-ux-1.44.0.mjs', 'verify:module-extension-workspace fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:module-extension-workspace'), 'verify:all enthält Workspace-Gate nicht');
expect(!/\bfetch\s*\(/.test(js), 'UX-Layer darf keine Netzwerkaufrufe ausführen');
expect(!/\b(?:POST|PUT|PATCH|DELETE)\b/.test(js), 'UX-Layer darf keine Schreiboperationen implementieren');
expect(!/\.innerHTML\s*=/.test(js), 'UX-Layer darf innerHTML nicht setzen');
expect(!/confirm\s*\(/.test(js), 'Kritische Bestätigungen bleiben im produktiven Modulmanager');
expect(!/VendooModuleManager\.(?:load|install|enable|disable)/.test(js), 'UX-Layer darf produktive Modulaktionen nicht direkt ausführen');
expect(!/setInterval\s*\(/.test(js), 'UX-Layer darf kein Polling starten');
if (failures.length) {
console.error('Module & Extension Workspace UX Gate fehlgeschlagen:');
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Module & Extension Workspace UX Gate bestanden.');
+138
View File
@@ -0,0 +1,138 @@
import fs from 'node:fs';
import path from 'node:path';
import vm from 'node:vm';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const source = read('public/navigation-api.js');
const loader = read('public/module-extension-workspace.js');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => { if (!condition) failures.push(message); };
const includesAll = (text, values, label) => values.forEach(value => expect(text.includes(value), `${label}: ${value}`));
includesAll(source, [
'uxNavigationApiReady',
'window.VendooNavigation = Object.freeze(api)',
'currentView',
'canOpen',
'open',
'vendoo:navigation-changed',
'vendoo:navigation-api-ready',
"version: '1.44.0'",
"attributeFilter: ['class', 'hidden', 'aria-hidden', 'aria-disabled']",
], 'Navigation-API-Vertrag fehlt');
includesAll(loader, [
'loadGlobalNavigationAssets',
'loadNavigationApi',
'navigation-api.js?v=1.44.0',
'data-ux-navigation-api',
'loadSafeActionAdapters',
], 'Navigation-API-Loader fehlt');
expect(loader.indexOf('loadGlobalNavigationAssets();') < loader.indexOf('loadNavigationApi();'), 'Global Navigation muss vor Navigation API geladen werden');
expect(loader.indexOf('loadNavigationApi();') < loader.indexOf('loadSafeActionAdapters();'), 'Navigation API muss vor Safe Action Adapters geladen werden');
expect(pkg.scripts?.['verify:navigation-api'] === 'node tools/verify-navigation-api-1.44.0.mjs', 'verify:navigation-api fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:navigation-api'), 'verify:all enthält Navigation-API-Gate nicht');
expect(!/\.innerHTML\s*=/.test(source), 'Navigation API darf innerHTML nicht setzen');
expect(!/\bfetch\s*\(/.test(source), 'Navigation API darf keine Netzwerkaufrufe ausführen');
expect(!/setInterval\s*\(/.test(source), 'Navigation API darf kein Polling starten');
expect(!/history\.(?:pushState|replaceState|back|forward)\s*\(/.test(source), 'Navigation API darf keine eigene History-Logik einführen');
expect(!/location\.(?:assign|replace)|window\.open\s*\(/.test(source), 'Navigation API darf keine externe Navigation ausführen');
function verifyRuntime() {
const events = [];
const listeners = new Map();
let observerConfig = null;
let observerDisconnected = false;
const classes = values => ({
values: new Set(values),
contains(value) { return this.values.has(value); },
add(value) { this.values.add(value); },
remove(value) { this.values.delete(value); },
});
const nav = { id: 'sidebar', querySelectorAll: () => items };
const item = (view, active = false) => ({
dataset: { tab: view },
hidden: false,
classList: classes(active ? ['nav-item', 'active'] : ['nav-item']),
attributes: new Map(),
clicks: 0,
getAttribute(name) { return this.attributes.get(name) ?? null; },
click() {
this.clicks += 1;
items.forEach(candidate => candidate.classList.remove('active'));
this.classList.add('active');
},
});
const dashboard = item('dashboard', true);
const history = item('history');
const hidden = item('hidden-view');
hidden.classList.add('hidden');
const items = [dashboard, history, hidden];
const documentObject = {
documentElement: { dataset: {} },
querySelector(selector) {
if (selector === '.sidebar-nav') return nav;
if (selector === '.sidebar-nav .nav-item.active[data-tab]') return items.find(candidate => candidate.classList.contains('active')) || null;
if (selector === '.tab-content.active') return null;
return null;
},
querySelectorAll(selector) {
return selector === '.sidebar-nav .nav-item[data-tab]' ? items : [];
},
};
class MutationObserverMock {
constructor(callback) { this.callback = callback; }
observe(target, config) { observerConfig = { target, config, observer: this }; }
disconnect() { observerDisconnected = true; }
}
const windowObject = {
addEventListener(type, listener) { listeners.set(type, listener); },
dispatchEvent(event) { events.push(event); return true; },
};
const context = vm.createContext({
document: documentObject,
window: windowObject,
MutationObserver: MutationObserverMock,
CustomEvent: class CustomEvent { constructor(type, init = {}) { this.type = type; this.detail = init.detail; } },
console,
});
vm.runInContext(source, context, { filename: 'public/navigation-api.js' });
const api = windowObject.VendooNavigation;
expect(Object.isFrozen(api), 'Runtime: Navigation API ist nicht schreibgeschützt');
expect(api.currentView === 'dashboard', 'Runtime: currentView erkennt die aktive Ansicht nicht');
expect(api.canOpen('history') === true, 'Runtime: erlaubte Ansicht wird nicht erkannt');
expect(api.canOpen('hidden-view') === false, 'Runtime: versteckte Ansicht wird als verfügbar gemeldet');
expect(api.canOpen('missing') === false, 'Runtime: unbekannte Ansicht wird als verfügbar gemeldet');
expect(api.open('hidden-view') === false && hidden.clicks === 0, 'Runtime: versteckte Ansicht wurde geöffnet');
expect(api.open('history') === true && history.clicks === 1, 'Runtime: Navigation delegiert nicht an den bestehenden Nav-Handler');
expect(api.currentView === 'history', 'Runtime: currentView folgt der bestehenden Navigation nicht');
observerConfig?.observer.callback();
expect(events.some(event => event.type === 'vendoo:navigation-api-ready' && event.detail?.version === '1.44.0'), 'Runtime: Ready-Event fehlt');
expect(events.some(event => event.type === 'vendoo:navigation-changed' && event.detail?.currentView === 'history' && event.detail?.previousView === 'dashboard'), 'Runtime: Change-Event fehlt');
expect(observerConfig?.target === nav, 'Runtime: Navigation wird nicht beobachtet');
expect(observerConfig?.config?.attributeFilter?.includes('aria-disabled'), 'Runtime: Observer-Vertrag ist unvollständig');
listeners.get('beforeunload')?.();
expect(observerDisconnected, 'Runtime: Navigation-Observer wird nicht getrennt');
}
verifyRuntime();
if (failures.length) {
console.error('Navigation API Gate fehlgeschlagen:');
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Navigation API Gate bestanden.');
+73
View File
@@ -0,0 +1,73 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const root = resolve(import.meta.dirname, '..');
const read = path => readFileSync(resolve(root, path), 'utf8');
const failures = [];
function requireText(content, needle, label) {
if (!content.includes(needle)) failures.push(`${label}: ${needle}`);
}
function forbidText(content, needle, label) {
if (content.includes(needle)) failures.push(`${label}: ${needle}`);
}
const loader = read('public/ux-foundation.js');
const script = read('public/publish-center-ux.js');
const style = read('public/publish-center-ux.css');
const pkg = JSON.parse(read('package.json'));
requireText(loader, '/publish-center-ux.css?v=1.44.0', 'Publish-CSS wird nicht geladen');
requireText(loader, '/publish-center-ux.js?v=1.44.0', 'Publish-JavaScript wird nicht geladen');
requireText(loader, 'loadPublishCenterAssets();', 'Publish-Assets fehlen in der UX-Initialisierung');
if (pkg.scripts?.['verify:publish-center'] !== 'node tools/verify-publish-center-ux-1.44.0.mjs') {
failures.push('package.json: verify:publish-center fehlt oder zeigt auf die falsche Datei');
}
requireText(pkg.scripts?.['verify:all'] || '', 'npm run verify:publish-center', 'Publish-Gate fehlt in verify:all');
for (const token of [
'OPERATIONAL_VIEWS',
'VIEW_META',
'ux-publish-view-groups',
'ux-publish-status-drawer',
'data-publish-operational',
'data-has-publish-selection',
'MutationObserver',
"button.setAttribute('role', 'tab')",
"button.setAttribute('aria-selected'",
'publish-filter-platform',
'publish-filter-status',
'publish-list-card',
]) requireText(script, token, 'Publish-UX-Vertrag unvollständig');
for (const forbidden of ['fetch(', 'api(', 'XMLHttpRequest', '.innerHTML', 'outerHTML', 'onclick=', 'localStorage.clear']) {
forbidText(script, forbidden, 'Publish-UX darf keine Daten-, API- oder unsichere DOM-Logik enthalten');
}
for (const token of [
'.ux-publish-guide',
'.ux-publish-view-groups',
'.ux-publish-status-drawer',
'[data-publish-operational]',
'[data-has-publish-selection]',
'.ux-publish-active-filters',
'.ux-publish-batch-more',
':focus-visible',
'@media (max-width: 900px)',
'@media (prefers-reduced-motion: reduce)',
'var(--vd-',
]) requireText(style, token, 'Publish-CSS-Vertrag unvollständig');
for (const forbidden of ['position: fixed', 'z-index: 9999', '#ff0000', '!important']) {
forbidText(style, forbidden, 'Publish-CSS verletzt Design- oder Layoutgrenzen');
}
if (failures.length) {
console.error('Publish-Center-UX 1.44.0 Gate fehlgeschlagen:');
for (const failure of failures) console.error(`- ${failure}`);
process.exit(1);
}
console.log('Publish-Center-UX 1.44.0 Gate bestanden.');
+112
View File
@@ -0,0 +1,112 @@
import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const js = read('public/quality-center-ux.js');
const css = read('public/quality-center-ux.css');
const runtime = read('public/design-system/theme-runtime.js');
const html = read('public/index.html');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => {
if (!condition) failures.push(message);
};
const includesAll = (source, values, label) => {
for (const value of values) expect(source.includes(value), `${label}: ${value}`);
};
includesAll(html, [
'id="quality-center"',
'id="quality-summary-average"',
'id="quality-summary-ready"',
'id="quality-summary-blocked"',
'id="quality-summary-total"',
'id="quality-search"',
'id="quality-status"',
'id="quality-grade"',
'id="quality-list"',
'id="quality-detail-content"',
'id="quality-readiness"',
'id="quality-open-article"',
'id="quality-open-ai"',
'id="quality-run-analysis"',
'id="quality-issues"',
'data-quality-issue-filter="error"',
'class="quality-duplicates"',
'class="quality-images"',
'class="quality-history"',
], 'Bestehender Quality-Center-Vertrag fehlt');
includesAll(runtime, [
'quality-center-ux.css?v=1.44.0',
'quality-center-ux.js?v=1.44.0',
'data-ux-quality-center',
'loadQualityCenterAssets()',
], 'Theme-Runtime bindet Quality Center nicht vollständig ein');
expect(pkg.scripts?.['verify:quality-center-ux'] === 'node tools/verify-quality-center-ux-1.44.0.mjs', 'verify:quality-center-ux Script fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:quality-center-ux'), 'verify:all enthält das Quality-Center-Gate nicht');
includesAll(js, [
'ux-quality-priority-board',
'data-ux-quality-status',
'ux-quality-filter-panel',
'ux-quality-filter-chips',
'ux-quality-main-actions',
'ux-quality-more-actions',
'ux-quality-issue-section',
'ux-quality-secondary',
'quality-summary-blocked',
'quality-summary-ready',
'quality-summary-total',
'quality-status',
'quality-open-article',
'quality-open-ai',
'quality-run-analysis',
'quality-readiness',
'quality-issues',
'MutationObserver',
'requestAnimationFrame',
'scrollIntoView',
'aria-pressed',
'ArrowLeft',
'ArrowRight',
], 'Quality-Center-UX-Vertrag fehlt');
includesAll(css, [
'#quality-center.ux-quality-center-ready',
'.ux-quality-priority-board',
'.ux-quality-priorities',
'.ux-quality-filter-panel',
'.ux-quality-filter-chips',
'.ux-quality-main-actions',
'.ux-quality-issue-section',
'.ux-quality-secondary',
'@media (max-width: 760px)',
'@media (max-width: 470px)',
'@media (prefers-reduced-motion: reduce)',
':focus-visible',
'var(--',
], 'Quality-Center-CSS-Vertrag fehlt');
expect(!/\bfetch\s*\(/.test(js), 'Additive Quality-Center-UX darf keine eigenen Netzwerkaufrufe ausführen');
expect(!/\bapi\s*\(/.test(js), 'Additive Quality-Center-UX darf die Vendoo-API nicht direkt aufrufen');
expect(!/\.innerHTML\s*=/.test(js), 'Quality-Center-UX darf keine innerHTML-Zuweisungen verwenden');
expect(!/\.outerHTML\s*=/.test(js), 'Quality-Center-UX darf keine outerHTML-Zuweisungen verwenden');
expect(!/\.onclick\s*=/.test(js), 'Quality-Center-UX darf keine onclick-Zuweisungen verwenden');
expect(!/confirm\s*\(/.test(js), 'Gefährliche Quality-Center-Aktionen müssen in der bestehenden geprüften Logik bleiben');
expect(!/\b(?:DELETE|PATCH|POST|PUT)\b/.test(js), 'Quality-Center-UX darf keine Schreiboperationen implementieren');
expect(!/runQualityAnalysis\s*\(/.test(js), 'UX-Schicht darf keine Qualitätsanalyse direkt auslösen');
expect(!/generateQualityAiSuggestion\s*\(/.test(js), 'UX-Schicht darf keine AI-Korrektur direkt auslösen');
expect(!/applyQualityAiSuggestion\s*\(/.test(js), 'UX-Schicht darf keine AI-Vorschläge direkt anwenden');
expect(!/state\.quality/.test(js), 'Additive UX darf den produktiven Quality-State nicht direkt verändern');
if (failures.length) {
console.error('Quality Center UX Gate fehlgeschlagen:');
for (const failure of failures) console.error(`- ${failure}`);
process.exit(1);
}
console.log('Quality Center UX Gate bestanden.');
@@ -0,0 +1,250 @@
import fs from 'node:fs';
import path from 'node:path';
import vm from 'node:vm';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const adapters = read('public/safe-action-adapters.js');
const loader = read('public/module-extension-workspace.js');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => { if (!condition) failures.push(message); };
const includesAll = (source, values, label) => values.forEach(value => expect(source.includes(value), `${label}: ${value}`));
const actionIds = [
'workspace:modules:refresh',
'workspace:modules:filter-all',
'workspace:modules:filter-active',
'workspace:modules:filter-disabled',
'workspace:modules:filter-external',
'workspace:modules:filter-problems',
'workspace:extensions:filter-all',
'workspace:extensions:filter-ready',
'workspace:extensions:filter-connected',
'workspace:extensions:filter-attention',
'workspace:articles:open-current',
'workspace:articles:view-cards',
'workspace:articles:view-list',
'workspace:articles:reset-filters',
'workspace:media:open-current-preview',
'workspace:media:density-compact',
'workspace:media:density-comfortable',
'workspace:media:density-large',
'workspace:media:view-grid',
'workspace:media:view-list',
'workspace:media:reset-filters',
];
includesAll(adapters, [
'uxSafeActionAdaptersReady',
'uxSafeActionAdaptersRegistered',
'window.VendooActions',
'window.VendooNavigation?.currentView',
'workspaceVisible',
"workspaceVisible('admin', 'admin-modules-panel')",
"workspaceVisible('extensions')",
"workspaceVisible('history')",
"workspaceVisible('media-library')",
'buttonAction',
'actions.registerMany(registered)',
"surfaces: ['command-palette']",
"danger: 'none'",
'vendoo:actions-ready',
'vendoo:safe-action-adapters-ready',
'baselineCount: 9',
'expansionCount: registered.length - 9',
...actionIds,
'#module-manager-refresh',
'#history-reset-filters',
'#media-view-grid',
'#media-view-list',
'[data-article-view-choice="cards"]',
'[data-article-view-choice="list"]',
'[data-media-density-option="compact"]',
'[data-media-density-option="comfortable"]',
'[data-media-density-option="large"]',
'.ux-media-filter-reset',
'focusin',
'pointerdown',
], 'Safe Action Adapter-Vertrag fehlt');
includesAll(loader, [
'loadCommandPlatformAssets',
'loadActionRegistryAssets',
'loadGlobalNavigationAssets',
'loadSafeActionAdapters',
'safe-action-adapters.js?v=1.44.0',
'data-ux-safe-action-adapters',
'loadSafeActionAdapters();',
], 'Safe-Action-Loader fehlt');
expect(loader.indexOf('loadActionRegistryAssets();') < loader.indexOf('loadSafeActionAdapters();'), 'Action Registry muss vor Safe Action Adapters geladen werden');
expect(loader.indexOf('loadGlobalNavigationAssets();') < loader.indexOf('loadSafeActionAdapters();'), 'Globale Navigation muss vor Safe Action Adapters geladen werden');
expect(pkg.scripts?.['verify:safe-action-adapters'] === 'node tools/verify-safe-action-adapters-1.44.0.mjs', 'verify:safe-action-adapters fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:safe-action-adapters'), 'verify:all enthält Safe-Action-Gate nicht');
expect(!/\.innerHTML\s*=/.test(adapters), 'Safe Action Adapters dürfen innerHTML nicht setzen');
expect(!/\bfetch\s*\(/.test(adapters), 'Safe Action Adapters dürfen keine Netzwerkaufrufe ausführen');
expect(!/\bconfirm\s*\(/.test(adapters), 'Safe Action Adapters dürfen window.confirm nicht verwenden');
expect(!/setInterval\s*\(/.test(adapters), 'Safe Action Adapters dürfen kein Polling starten');
expect(!/Vendoo(?:ModuleManager|App|Api)\.(?:publish|delete|remove|install|enable|disable|start|stop)/i.test(adapters), 'Safe Action Adapters dürfen keine produktive Fachlogik direkt aufrufen');
expect(!/danger:\s*['"](?:caution|destructive)['"]/.test(adapters), 'Safe Action Adapters dürfen keine vorsichtigen oder destruktiven Actions registrieren');
function element({ id = '', selector = '', parent = null } = {}) {
return {
id,
selector,
hidden: false,
disabled: false,
isConnected: true,
clicks: 0,
classList: { contains: () => false },
getAttribute: () => null,
click() { this.clicks += 1; },
closest(query) {
if (query === '#history-list .history-row' && parent?.kind === 'article-row') return parent;
if (query === '[data-media-preview]' && this.selector === '[data-media-preview]') return this;
if (query === '.history-row' && parent?.kind === 'article-row') return parent;
if (query === '.media-card' && parent?.kind === 'media-card') return parent;
return null;
},
querySelector(query) { return parent?.querySelector?.(query) || null; },
};
}
function verifyRuntime() {
const registered = [];
const documentListeners = new Map();
const events = [];
const rootElement = { dataset: {} };
const sectionMap = new Map([
['admin-modules-panel', element({ id: 'admin-modules-panel' })],
['extensions', element({ id: 'extensions' })],
['history', element({ id: 'history' })],
['media-library', element({ id: 'media-library' })],
]);
const selectors = [
'#module-manager-refresh',
'[data-ux-module-group="modules"][data-ux-module-filter="all"]',
'[data-ux-module-group="modules"][data-ux-module-filter="active"]',
'[data-ux-module-group="modules"][data-ux-module-filter="disabled"]',
'[data-ux-module-group="modules"][data-ux-module-filter="external"]',
'[data-ux-module-group="modules"][data-ux-module-filter="problems"]',
'[data-ux-module-group="extensions"][data-ux-module-filter="all"]',
'[data-ux-module-group="extensions"][data-ux-module-filter="ready"]',
'[data-ux-module-group="extensions"][data-ux-module-filter="connected"]',
'[data-ux-module-group="extensions"][data-ux-module-filter="attention"]',
'[data-article-view-choice="cards"]',
'[data-article-view-choice="list"]',
'#history-reset-filters',
'[data-media-density-option="compact"]',
'[data-media-density-option="comfortable"]',
'[data-media-density-option="large"]',
'#media-view-grid',
'#media-view-list',
'.ux-media-filter-reset',
];
const selectorMap = new Map(selectors.map(selector => [selector, element({ selector })]));
const articleTitle = element({ selector: '.history-article-title' });
const articleRow = {
kind: 'article-row',
hidden: false,
disabled: false,
isConnected: true,
getAttribute: () => null,
querySelector: query => query === '.history-article-title' ? articleTitle : null,
};
const articleTarget = element({ parent: articleRow });
const mediaPreview = element({ selector: '[data-media-preview]' });
const documentObject = {
documentElement: rootElement,
addEventListener(type, listener) { documentListeners.set(type, listener); },
getElementById(id) { return sectionMap.get(id) || null; },
querySelector(selector) { return selectorMap.get(selector) || null; },
};
const windowObject = {
VendooNavigation: { currentView: 'admin' },
VendooActions: {
registerMany(actions) { registered.push(...actions); },
refresh() {},
},
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
addEventListener() {},
dispatchEvent(event) { events.push(event); return true; },
};
const context = vm.createContext({
window: windowObject,
document: documentObject,
CustomEvent: class CustomEvent { constructor(type, init = {}) { this.type = type; this.detail = init.detail; } },
console,
});
vm.runInContext(adapters, context, { filename: 'public/safe-action-adapters.js' });
expect(registered.length === 21, `Runtime: erwartet 21 Adapter-Actions, erhalten ${registered.length}`);
expect(new Set(registered.map(action => action.id)).size === registered.length, 'Runtime: Action-IDs sind nicht eindeutig');
expect(actionIds.every(id => registered.some(action => action.id === id)), 'Runtime: mindestens eine verbindliche Action-ID fehlt');
expect(registered.every(action => action.danger === 'none'), 'Runtime: nicht sichere Gefahrstufe registriert');
expect(registered.every(action => action.surfaces?.length === 1 && action.surfaces[0] === 'command-palette'), 'Runtime: unerlaubte Action-Surface registriert');
const byId = id => registered.find(action => action.id === id);
const clicked = selector => selectorMap.get(selector)?.clicks === 1;
const invoke = (id, selector, message) => {
expect(byId(id)?.enabled() === true, `${message}: Action ist deaktiviert`);
expect(byId(id)?.handler() === true && clicked(selector), `${message}: Delegation fehlt`);
};
expect(byId('workspace:modules:refresh')?.visible() === true, 'Runtime: Modulaktion ist in Verwaltung nicht sichtbar');
expect(byId('workspace:extensions:filter-all')?.visible() === false, 'Runtime: Extension-Aktion ist außerhalb des Extension-Bereichs sichtbar');
expect(byId('workspace:articles:view-list')?.visible() === false, 'Runtime: Artikelaktion ist außerhalb des Artikelbereichs sichtbar');
invoke('workspace:modules:refresh', '#module-manager-refresh', 'Modul-Refresh');
invoke('workspace:modules:filter-active', '[data-ux-module-group="modules"][data-ux-module-filter="active"]', 'Aktive Module');
invoke('workspace:modules:filter-disabled', '[data-ux-module-group="modules"][data-ux-module-filter="disabled"]', 'Deaktivierte Module');
invoke('workspace:modules:filter-external', '[data-ux-module-group="modules"][data-ux-module-filter="external"]', 'Installierte Module');
invoke('workspace:modules:filter-problems', '[data-ux-module-group="modules"][data-ux-module-filter="problems"]', 'Problematische Module');
windowObject.VendooNavigation.currentView = 'extensions';
expect(byId('workspace:modules:refresh')?.visible() === false, 'Runtime: Modulaktion bleibt außerhalb der Verwaltung sichtbar');
expect(byId('workspace:extensions:filter-connected')?.visible() === true, 'Runtime: Extension-Aktion ist im Extension-Bereich nicht sichtbar');
invoke('workspace:extensions:filter-ready', '[data-ux-module-group="extensions"][data-ux-module-filter="ready"]', 'Bereite Extensions');
invoke('workspace:extensions:filter-connected', '[data-ux-module-group="extensions"][data-ux-module-filter="connected"]', 'Verbundene Extensions');
invoke('workspace:extensions:filter-attention', '[data-ux-module-group="extensions"][data-ux-module-filter="attention"]', 'Extensions mit Handlungsbedarf');
windowObject.VendooNavigation.currentView = 'history';
expect(byId('workspace:articles:view-list')?.visible() === true, 'Runtime: Artikelaktion ist im Artikelbereich nicht sichtbar');
invoke('workspace:articles:view-list', '[data-article-view-choice="list"]', 'Artikel-Listenansicht');
invoke('workspace:articles:reset-filters', '#history-reset-filters', 'Artikelfilter-Reset');
documentListeners.get('focusin')?.({ target: articleTarget });
expect(byId('workspace:articles:open-current')?.enabled() === true, 'Runtime: fokussierter Artikel wird nicht als ausführbar erkannt');
expect(byId('workspace:articles:open-current')?.handler() === true && articleTitle.clicks === 1, 'Runtime: Artikel öffnen delegiert nicht an bestehenden Titel-Handler');
windowObject.VendooNavigation.currentView = 'media-library';
expect(byId('workspace:media:view-grid')?.visible() === true, 'Runtime: Medienaktion ist im Medienbereich nicht sichtbar');
invoke('workspace:media:density-compact', '[data-media-density-option="compact"]', 'Kompakte Galeriedichte');
invoke('workspace:media:density-large', '[data-media-density-option="large"]', 'Große Galeriedichte');
invoke('workspace:media:view-grid', '#media-view-grid', 'Medien-Rasteransicht');
invoke('workspace:media:view-list', '#media-view-list', 'Medien-Listenansicht');
invoke('workspace:media:reset-filters', '.ux-media-filter-reset', 'Medienfilter-Reset');
documentListeners.get('pointerdown')?.({ target: mediaPreview });
expect(byId('workspace:media:open-current-preview')?.enabled() === true, 'Runtime: fokussierte Medienvorschau wird nicht als ausführbar erkannt');
expect(byId('workspace:media:open-current-preview')?.handler() === true && mediaPreview.clicks === 1, 'Runtime: Medienvorschau delegiert nicht an bestehenden Preview-Handler');
expect(events.some(event => event.type === 'vendoo:safe-action-adapters-ready'
&& event.detail?.count === 21
&& event.detail?.baselineCount === 9
&& event.detail?.expansionCount === 12), 'Runtime: vollständiges Ready-Event fehlt');
}
verifyRuntime();
if (failures.length) {
console.error('Safe Action Adapter Gate fehlgeschlagen:');
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Safe Action Adapter Gate bestanden: 21 sichere Actions.');
@@ -0,0 +1,69 @@
import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const js = read('public/settings-control-center.js');
const css = read('public/settings-control-center.css');
const runtime = read('public/design-system/theme-runtime.js');
const html = read('public/index.html');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => { if (!condition) failures.push(message); };
const includesAll = (source, values, label) => values.forEach(value => expect(source.includes(value), `${label}: ${value}`));
includesAll(html, [
'id="settings"', 'class="settings-hero"', 'class="settings-shell"', 'class="settings-nav"',
'data-settings-panel="general"', 'data-settings-panel="appearance"', 'data-settings-panel="ai"',
'data-settings-panel="marketplaces"', 'data-settings-panel="flux"',
'id="settings-ai-status-text"', 'id="settings-flux-status-text"',
'id="etsy-status-badge"', 'id="ebay-status-badge"', 'id="save-settings-btn"'
], 'Bestehender Einstellungen-Vertrag fehlt');
includesAll(runtime, [
'settings-control-center.css?v=1.44.0',
'settings-control-center.js?v=1.44.0',
'data-ux-settings-control-center',
'loadSettingsControlCenterAssets()'
], 'Theme-Runtime bindet das Settings-Control-Center nicht vollständig ein');
expect(pkg.scripts?.['verify:settings-control-center'] === 'node tools/verify-settings-control-center-ux-1.44.0.mjs', 'verify:settings-control-center Script fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:settings-control-center'), 'verify:all enthält das Settings-Gate nicht');
includesAll(js, [
'ux-settings-control-center-ready', 'ux-settings-command', 'ux-settings-search-input',
'ux-settings-search-result', 'ux-settings-health', 'ux-settings-health-card',
'ux-settings-category-grid', 'ux-settings-category-card', 'ux-settings-advanced',
'dataset.uxSettingsCategory', 'dataset.uxSettingsHealth',
'settings-ai-status-text', 'settings-flux-status-text', 'etsy-status-badge', 'ebay-status-badge',
'MutationObserver', 'requestAnimationFrame', 'scrollIntoView', 'aria-live', 'aria-pressed',
'localStorage', 'dataset.reducedMotion'
], 'Settings-Control-Center-JS-Vertrag fehlt');
includesAll(css, [
'#settings.ux-settings-control-center-ready', '.ux-settings-command', '.ux-settings-search',
'.ux-settings-health-card', '.ux-settings-category-grid', '.ux-settings-category-card',
'.ux-settings-advanced', '.ux-settings-search-hit', '.settings-savebar',
'@media (max-width:1100px)', '@media (max-width:780px)', '@media (max-width:520px)',
'@media (prefers-reduced-motion:reduce)', ':focus-visible', 'var(--'
], 'Settings-Control-Center-CSS-Vertrag fehlt');
expect(!/\bfetch\s*\(/.test(js), 'Additive Settings-UX darf keine eigenen Netzwerkaufrufe ausführen');
expect(!/\bapi\s*\(/.test(js), 'Additive Settings-UX darf die Vendoo-API nicht direkt aufrufen');
expect(!/\.innerHTML\s*=/.test(js), 'Settings-UX darf keine innerHTML-Zuweisungen verwenden');
expect(!/\.outerHTML\s*=/.test(js), 'Settings-UX darf keine outerHTML-Zuweisungen verwenden');
expect(!/\.onclick\s*=/.test(js), 'Settings-UX darf keine onclick-Zuweisungen verwenden');
expect(!/confirm\s*\(/.test(js), 'Gefährliche Aktionen müssen in der bestehenden geprüften Logik bleiben');
expect(!/\b(?:DELETE|PATCH|POST|PUT)\b/.test(js), 'Settings-UX darf keine Schreiboperationen implementieren');
expect(!/saveSettings\s*\(/.test(js), 'Settings-UX darf Einstellungen nicht selbst speichern');
expect(!/testLocalImageProvider\s*\(/.test(js), 'Settings-UX darf FLUX-Tests nicht selbst ausführen');
expect(!/setInterval\s*\(/.test(js), 'Settings-UX darf keinen eigenen Polling-Zyklus starten');
if (failures.length) {
console.error('Settings Control Center UX Gate fehlgeschlagen:');
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Settings Control Center UX Gate bestanden.');
@@ -0,0 +1,111 @@
import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const js = read('public/template-workspace.js');
const css = read('public/template-workspace.css');
const runtime = read('public/design-system/theme-runtime.js');
const html = read('public/index.html');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => {
if (!condition) failures.push(message);
};
const includesAll = (source, values, label) => {
for (const value of values) expect(source.includes(value), `${label}: ${value}`);
};
includesAll(html, [
'id="templates"',
'class="templates-header"',
'id="add-template-btn"',
'id="template-list"',
'id="template-form"',
'id="template-form-title"',
'id="tpl-name"',
'id="tpl-platform"',
'id="tpl-language"',
'id="tpl-notes-editor"',
'id="tpl-tags"',
'id="save-template-btn"',
'id="cancel-template-btn"',
], 'Bestehender Vorlagenvertrag fehlt');
includesAll(runtime, [
'template-workspace.css?v=1.44.0',
'template-workspace.js?v=1.44.0',
'data-ux-template-workspace',
'loadTemplateWorkspaceAssets()',
], 'Theme-Runtime bindet die Vorlagen-UX nicht vollständig ein');
expect(pkg.scripts?.['verify:template-workspace'] === 'node tools/verify-template-workspace-ux-1.44.0.mjs', 'verify:template-workspace Script fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:template-workspace'), 'verify:all enthält das Vorlagen-UX-Gate nicht');
includesAll(js, [
'vendoo_template_workspace_filters',
'ux-template-hero',
'ux-template-metrics',
'ux-template-toolbar',
'uxTemplateSearch',
'uxTemplatePlatform',
'uxTemplateLanguage',
'uxTemplateInitial',
'ux-template-card',
'ux-template-badges',
'ux-template-more-actions',
'ux-template-editor-head',
'ux-template-editor-layout',
'ux-template-editor-actions',
'updateEditorStatus',
'template-item',
'tpl-edit',
'tpl-delete',
'save-template-btn',
'cancel-template-btn',
'MutationObserver',
'requestAnimationFrame',
'scrollIntoView',
'aria-live',
'ArrowLeft',
'ArrowRight',
], 'Vorlagen-UX-Vertrag fehlt');
includesAll(css, [
'#templates.ux-template-workspace-ready',
'.ux-template-hero',
'.ux-template-toolbar',
'.ux-template-card',
'.ux-template-badges',
'.ux-template-more-actions',
'#templates.ux-template-editor-open',
'#templates.ux-template-list-open',
'.ux-template-editor-layout',
'.ux-template-editor-actions',
'@media (max-width: 700px)',
'@media (max-width: 470px)',
'@media (prefers-reduced-motion: reduce)',
':focus-visible',
'var(--',
], 'Vorlagen-UX-CSS-Vertrag fehlt');
expect(!/\bfetch\s*\(/.test(js), 'Additive Vorlagen-UX darf keine eigenen Netzwerkaufrufe ausführen');
expect(!/\bapi\s*\(/.test(js), 'Additive Vorlagen-UX darf die Vendoo-API nicht direkt aufrufen');
expect(!/\.innerHTML\s*=/.test(js), 'Vorlagen-UX darf keine innerHTML-Zuweisungen verwenden');
expect(!/\.outerHTML\s*=/.test(js), 'Vorlagen-UX darf keine outerHTML-Zuweisungen verwenden');
expect(!/\.onclick\s*=/.test(js), 'Vorlagen-UX darf keine onclick-Zuweisungen verwenden');
expect(!/confirm\s*\(/.test(js), 'Gefährliche Vorlagenaktionen müssen in der bestehenden geprüften Logik bleiben');
expect(!/\b(?:DELETE|PATCH|POST|PUT)\b/.test(js), 'Vorlagen-UX darf keine Schreiboperationen implementieren');
expect(!/setupTemplates\s*\(/.test(js), 'Vorlagen-UX darf den produktiven Setup-Ablauf nicht direkt aufrufen');
expect(!/renderTemplateList\s*\(/.test(js), 'Vorlagen-UX darf die produktive Vorlagenliste nicht direkt rendern');
expect(!/updateTemplateDropdown\s*\(/.test(js), 'Vorlagen-UX darf den produktiven Generator-Dropdown nicht direkt verändern');
expect(!/state\.templates/.test(js), 'Additive Vorlagen-UX darf den produktiven Template-State nicht direkt verändern');
if (failures.length) {
console.error('Template Workspace UX Gate fehlgeschlagen:');
for (const failure of failures) console.error(`- ${failure}`);
process.exit(1);
}
console.log('Template Workspace UX Gate bestanden.');
+112
View File
@@ -0,0 +1,112 @@
import fs from 'node:fs';
import path from 'node:path';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const js = read('public/today-workspace.js');
const css = read('public/today-workspace.css');
const runtime = read('public/design-system/theme-runtime.js');
const html = read('public/index.html');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => {
if (!condition) failures.push(message);
};
const includesAll = (source, values, label) => {
for (const value of values) expect(source.includes(value), `${label}: ${value}`);
};
includesAll(html, [
'id="today"',
'today-summary-grid',
'id="today-count-ready"',
'id="today-count-scheduled"',
'id="today-count-published"',
'id="today-count-sold"',
'id="today-count-attention"',
'today-filterbar',
'today-ready-panel',
'today-scheduled-panel',
'attention-panel',
'today-activity-panel',
'id="studio-inspector"',
'id="studio-inspector-content"',
], 'Bestehender Heute-Vertrag fehlt');
includesAll(runtime, [
'today-workspace.css?v=1.44.0',
'today-workspace.js?v=1.44.0',
'data-ux-today-workspace',
'loadTodayWorkspaceAssets()',
], 'Theme-Runtime bindet den Heute-Arbeitsplatz nicht vollständig ein');
expect(pkg.scripts?.['verify:today-workspace'] === 'node tools/verify-today-workspace-ux-1.44.0.mjs', 'verify:today-workspace Script fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:today-workspace'), 'verify:all enthält das Heute-UX-Gate nicht');
includesAll(js, [
'ux-today-command',
'ux-today-command-actions',
'ux-today-shortcuts',
'ux-today-filter-details',
'ux-today-panel-toggle',
'ux-today-inspector-toggle',
'ux-today-has-attention',
'ux-today-no-attention',
'uxTodayPrimary',
'uxTodayTitle',
'uxTodayCopy',
'today-count-ready',
'today-count-scheduled',
'today-count-attention',
'today-ready-panel',
'today-scheduled-panel',
'attention-panel',
'today-activity-panel',
'studio-inspector-content',
'MutationObserver',
'requestAnimationFrame',
'scrollIntoView',
'ArrowLeft',
'ArrowRight',
'aria-expanded',
'aria-controls',
], 'Heute-UX-Vertrag fehlt');
includesAll(css, [
'#today.ux-today-workspace-ready',
'.ux-today-command',
'.ux-today-shortcuts',
'.ux-today-filter-details',
'.ux-today-panel-collapsed',
'.ux-today-inspector-toggle',
'.ux-today-has-attention',
'.ux-today-no-attention',
'@media (max-width: 900px)',
'@media (max-width: 700px)',
'@media (max-width: 470px)',
'@media (prefers-reduced-motion: reduce)',
':focus-visible',
'var(--',
], 'Heute-UX-CSS-Vertrag fehlt');
expect(!/\bfetch\s*\(/.test(js), 'Additive Heute-UX darf keine eigenen Netzwerkaufrufe ausführen');
expect(!/\bapi\s*\(/.test(js), 'Additive Heute-UX darf die Vendoo-API nicht direkt aufrufen');
expect(!/\.innerHTML\s*=/.test(js), 'Heute-UX darf keine innerHTML-Zuweisungen verwenden');
expect(!/\.outerHTML\s*=/.test(js), 'Heute-UX darf keine outerHTML-Zuweisungen verwenden');
expect(!/\.onclick\s*=/.test(js), 'Heute-UX darf keine onclick-Zuweisungen verwenden');
expect(!/confirm\s*\(/.test(js), 'Gefährliche Aktionen müssen in der bestehenden geprüften Logik bleiben');
expect(!/\b(?:DELETE|PATCH|POST|PUT)\b/.test(js), 'Heute-UX darf keine Schreiboperationen implementieren');
expect(!/loadToday\s*\(/.test(js), 'Heute-UX darf den produktiven Heute-Ladevorgang nicht direkt auslösen');
expect(!/loadPublishList\s*\(/.test(js), 'Heute-UX darf Publishing-Daten nicht direkt laden');
expect(!/loadHistory\s*\(/.test(js), 'Heute-UX darf Artikeldaten nicht direkt laden');
expect(!/setInterval\s*\(/.test(js), 'Heute-UX darf keinen eigenen Polling-Zyklus starten');
expect(!/(?:^|[^a-zA-Z])state\.[a-zA-Z]/m.test(js), 'Additive Heute-UX darf den produktiven App-State nicht direkt verändern');
if (failures.length) {
console.error('Today Workspace UX Gate fehlgeschlagen:');
for (const failure of failures) console.error(`- ${failure}`);
process.exit(1);
}
console.log('Today Workspace UX Gate bestanden.');
@@ -0,0 +1,218 @@
import fs from 'node:fs';
import path from 'node:path';
import vm from 'node:vm';
const root = process.cwd();
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const js = read('public/action-registry.js');
const css = read('public/action-registry.css');
const loader = read('public/module-extension-workspace.js');
const commandJs = read('public/command-platform.js');
const navigationJs = read('public/global-navigation-ux.js');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const expect = (condition, message) => { if (!condition) failures.push(message); };
const includesAll = (source, values, label) => values.forEach(value => expect(source.includes(value), `${label}: ${value}`));
includesAll(js, [
'uxActionRegistryReady',
'window.VendooActions',
'registerMany',
'resolveAction',
'evaluatePermission',
'selectionCount',
'surfaces',
'command-palette',
"kind: 'action'",
'single',
'parallel',
'requestConfirmation',
'setConfirmationProvider',
'setAuditSink',
'action-executing',
'action-executed',
'action-failed',
'action-blocked',
'action-cancelled',
'action-rolled-back',
'action-rollback-failed',
'action-shortcut-conflict',
'data-vendoo-action',
'renderSurface',
'mountSurface',
'normalizeShortcut',
'MutationObserver',
], 'Universal Action Registry JS-Vertrag fehlt');
includesAll(css, [
'.vd-action-surface',
'.vd-action-button',
'.vd-action-running',
'.vd-action-destructive',
'.vd-action-confirm-overlay',
'.vd-action-confirm-dialog',
'.vd-action-confirm-phrase',
'@media(max-width:620px)',
'@media(prefers-reduced-motion:reduce)',
':focus-visible',
'var(--',
], 'Universal Action Registry CSS-Vertrag fehlt');
includesAll(loader, [
'loadCommandPlatformAssets',
'loadActionRegistryAssets',
'action-registry.css?v=1.44.0',
'action-registry.js?v=1.44.0',
'data-ux-action-registry',
'loadGlobalNavigationAssets',
'loadCommandPlatformAssets();',
'loadActionRegistryAssets();',
'loadGlobalNavigationAssets();',
], 'Action-Registry-Loader fehlt');
expect(loader.indexOf('loadCommandPlatformAssets();') < loader.indexOf('loadActionRegistryAssets();'), 'Command Platform muss vor Action Registry geladen werden');
expect(loader.indexOf('loadActionRegistryAssets();') < loader.indexOf('loadGlobalNavigationAssets();'), 'Action Registry muss vor globaler Navigation geladen werden');
expect(commandJs.includes('window.VendooCommands'), 'Command Platform fehlt als Adapterziel');
expect(navigationJs.includes('registry.execute'), 'Command Palette delegiert nicht an die Command Registry');
expect(pkg.scripts?.['verify:universal-actions'] === 'node tools/verify-universal-action-registry-1.44.0.mjs', 'verify:universal-actions fehlt');
expect(pkg.scripts?.['verify:all']?.includes('npm run verify:universal-actions'), 'verify:all enthält Universal-Action-Gate nicht');
expect(!/\.innerHTML\s*=/.test(js), 'Action Registry darf innerHTML nicht setzen');
expect(!/\bfetch\s*\(/.test(js), 'Action Registry darf keine Netzwerkaufrufe ausführen');
expect(!/\b(?:POST|PUT|PATCH|DELETE)\b/.test(js), 'Action Registry darf keine HTTP-Schreiboperationen implementieren');
expect(!/\bconfirm\s*\(/.test(js), 'Destruktive Aktionen dürfen window.confirm nicht verwenden');
expect(!/setInterval\s*\(/.test(js), 'Action Registry darf kein Polling starten');
expect(!/Vendoo(?:ModuleManager|App|Api)\.(?:publish|delete|remove|install|enable|disable)/i.test(js), 'Action Registry darf produktive Fachaktionen nicht direkt implementieren');
async function verifyRuntime() {
const events = [];
const commandAdapters = new Map();
let commandUnregisters = 0;
const documentListeners = new Map();
const windowListeners = new Map();
const classSet = new Set();
const body = {
nodeType: 1,
classList: {
add: value => classSet.add(value),
remove: value => classSet.delete(value),
toggle: (value, force) => force ? classSet.add(value) : classSet.delete(value),
},
append() {},
querySelectorAll: () => [],
};
const documentObject = {
documentElement: { dataset: {} },
body,
activeElement: null,
addEventListener(type, listener) { documentListeners.set(type, listener); },
removeEventListener() {},
querySelectorAll: () => [],
createElement() { throw new Error('DOM-Erzeugung ist im Registry-Runtime-Test nicht erwartet.'); },
};
const windowObject = {
dispatchEvent(event) { events.push(event); return true; },
addEventListener(type, listener) { windowListeners.set(type, listener); },
VendooPermissions: undefined,
VendooNavigation: { currentView: 'dashboard' },
VendooCommands: {
register(command) {
commandAdapters.set(command.id, command);
return () => { commandUnregisters += 1; commandAdapters.delete(command.id); };
},
},
};
class Observer { observe() {} disconnect() {} }
windowObject.MutationObserver = Observer;
const context = vm.createContext({
window: windowObject,
document: documentObject,
CustomEvent: class CustomEvent { constructor(type, init = {}) { this.type = type; this.detail = init.detail; } },
MutationObserver: Observer,
requestAnimationFrame: callback => { callback(); return 1; },
cancelAnimationFrame() {},
console,
setTimeout,
clearTimeout,
});
vm.runInContext(js, context, { filename: 'public/action-registry.js' });
const actions = windowObject.VendooActions;
expect(actions && typeof actions.register === 'function', 'Runtime: VendooActions.register fehlt');
expect(typeof actions.renderSurface === 'function' && typeof actions.mountSurface === 'function', 'Runtime: Surface API fehlt');
expect(actions.normalizeShortcut('control + shift + p') === 'Ctrl+Shift+P', 'Runtime: Shortcut-Normalisierung fehlerhaft');
let executed = 0;
actions.register({
id: 'test:safe',
title: 'Sichere Aktion',
description: 'Runtime-Test',
group: 'Test',
surfaces: ['toolbar', 'command-palette'],
contexts: ['dashboard'],
selection: { min: 1, max: 2 },
handler: () => { executed += 1; return true; },
});
expect(commandAdapters.get('action:test:safe')?.kind === 'action', 'Runtime: Command-Palette-Adapter fehlt');
actions.refresh('test:safe');
expect(commandUnregisters === 0, 'Runtime: Status-Refresh darf Command-Adapter nicht abmelden und Favoriten verlieren');
expect(actions.list({ surface: 'toolbar', context: { scope: 'dashboard', selectionCount: 1 } })[0]?.id === 'test:safe', 'Runtime: Surface-/Kontextfilter liefert falsche Action');
expect(actions.resolve('test:safe', { scope: 'dashboard', selectionCount: 0 })?.enabled === false, 'Runtime: Mindestselektion wird nicht erzwungen');
expect(actions.resolve('test:safe', { scope: 'settings', selectionCount: 1 })?.visible === false, 'Runtime: Kontextgrenze wird nicht erzwungen');
expect(await actions.execute('test:safe', { scope: 'dashboard', selectionCount: 1, source: 'runtime' }) === true && executed === 1, 'Runtime: sichere Action wurde nicht ausgeführt');
let duplicateRejected = false;
try {
actions.register({ id: 'test:safe', title: 'Duplikat', handler: () => true });
} catch { duplicateRejected = true; }
expect(duplicateRejected, 'Runtime: doppelte Action-ID wurde nicht abgewiesen');
actions.register({ id: 'test:permission', title: 'Geschützte Aktion', permission: 'admin', handler: () => true });
expect(actions.resolve('test:permission')?.visible === false, 'Runtime: Permission ohne Resolver muss deny-by-default sein');
windowObject.VendooPermissions = { can: permission => permission === 'admin' };
expect(actions.resolve('test:permission')?.visible === true, 'Runtime: Permission-Resolver wird nicht ausgewertet');
let destructiveRuns = 0;
actions.register({ id: 'test:destructive', title: 'Löschen', danger: 'destructive', handler: () => { destructiveRuns += 1; } });
actions.setConfirmationProvider(() => false);
expect(await actions.execute('test:destructive', { source: 'runtime' }) === false && destructiveRuns === 0, 'Runtime: abgelehnte destruktive Action wurde ausgeführt');
actions.setConfirmationProvider(() => true);
expect(await actions.execute('test:destructive', { source: 'runtime' }) === true && destructiveRuns === 1, 'Runtime: bestätigte destruktive Action wurde nicht ausgeführt');
let releaseLongAction;
actions.register({
id: 'test:single-flight',
title: 'Langlauf',
concurrency: 'single',
handler: () => new Promise(resolve => { releaseLongAction = resolve; }),
});
const firstRun = actions.execute('test:single-flight', { source: 'runtime' });
await Promise.resolve();
expect(actions.resolve('test:single-flight')?.running === true, 'Runtime: Single-Flight-Status fehlt');
expect(await actions.execute('test:single-flight', { source: 'runtime' }) === false, 'Runtime: parallele Single-Flight-Ausführung wurde nicht blockiert');
releaseLongAction(true);
expect(await firstRun === true, 'Runtime: Single-Flight-Ausführung wurde nicht abgeschlossen');
let rollbackRuns = 0;
actions.register({
id: 'test:rollback',
title: 'Rollback-Test',
handler: () => { throw new Error('Fehler'); },
rollback: () => { rollbackRuns += 1; },
});
expect(await actions.execute('test:rollback', { source: 'runtime' }) === false && rollbackRuns === 1, 'Runtime: Rollback-Hook wurde nicht ausgeführt');
expect(events.some(event => event.type === 'vendoo:action-rolled-back' && event.detail?.actionId === 'test:rollback'), 'Runtime: Rollback-Event fehlt');
expect(events.some(event => event.type === 'vendoo:action-cancelled' && event.detail?.actionId === 'test:destructive'), 'Runtime: Cancel-Event fehlt');
expect(events.some(event => event.type === 'vendoo:action-executed' && event.detail?.actionId === 'test:safe'), 'Runtime: Executed-Event fehlt');
}
await verifyRuntime();
if (failures.length) {
console.error('Universal Action Registry Gate fehlgeschlagen:');
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Universal Action Registry Gate bestanden.');
+46
View File
@@ -0,0 +1,46 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const root = resolve(process.cwd());
const read = path => readFileSync(resolve(root, path), 'utf8');
const runtime = read('public/design-system/theme-runtime.js');
const css = read('public/ux-foundation.css');
const js = read('public/ux-foundation.js');
const pkg = JSON.parse(read('package.json'));
const failures = [];
const assert = (condition, message) => { if (!condition) failures.push(message); };
assert(runtime.includes('/ux-foundation.css?v=1.44.0'), 'UX-Foundation-Stylesheet wird nicht über den stabilen Theme-Runtime-Pfad geladen.');
assert(runtime.includes('/ux-foundation.js?v=1.44.0'), 'UX-Foundation-Script wird nicht über den stabilen Theme-Runtime-Pfad geladen.');
assert(runtime.includes("window.addEventListener('load', loadBehavior"), 'UX-Verhalten startet nicht nach der bestehenden App-Initialisierung.');
assert(pkg.scripts?.['verify:ux-foundation'] === 'node tools/verify-ux-foundation-1.44.0.mjs', 'npm-Script verify:ux-foundation fehlt oder ist falsch.');
assert(String(pkg.scripts?.['verify:all'] || '').includes('verify:ux-foundation'), 'verify:all enthält das UX-Foundation-Gate nicht.');
for (const label of ['Start', 'Verkaufen', 'Inhalte & AI', 'Werkzeuge & System']) {
assert(js.includes(`label: '${label}'`), `Navigationsgruppe fehlt: ${label}`);
}
for (const tab of ['dashboard', 'today', 'generator', 'history', 'publish', 'inventory', 'media-library', 'settings', 'admin']) {
assert(js.includes(tab), `Navigationsziel fehlt im UX-Vertrag: ${tab}`);
}
assert(js.includes('aria-current'), 'Aktive Navigation wird nicht über aria-current ausgezeichnet.');
assert(js.includes('MutationObserver'), 'Kontexttitel reagieren nicht auf Ansichtswechsel.');
assert(js.includes('data-ux-target'), 'Kontextbezogene Schnellaktionen fehlen.');
assert(js.includes("localStorage.setItem('vendoo_ux_system_nav_open'"), 'Zustand der Systemnavigation wird nicht benutzerfreundlich gespeichert.');
assert(!js.includes('onclick='), 'Inline-Eventhandler sind im UX-Script unzulässig.');
assert(!js.includes('innerHTML +=') && !js.includes('outerHTML'), 'Unsichere DOM-Erweiterung im UX-Script erkannt.');
for (const selector of ['.ux-nav-sections', '.ux-command-center', '.ux-dashboard-more', '@media (max-width: 900px)', '@media (prefers-reduced-motion: reduce)']) {
assert(css.includes(selector), `UX-CSS-Vertrag fehlt: ${selector}`);
}
assert(css.includes('focus-visible'), 'Sichtbare Tastaturfokusse fehlen.');
assert(css.includes('var(--'), 'UX-CSS verwendet das zentrale Token-/Variablensystem nicht.');
if (failures.length) {
console.error('[UX-FOUNDATION] Prüfung fehlgeschlagen:');
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('[UX-FOUNDATION] Navigation, Dashboard-Reduktion, Kontextführung, Runtime-Integration, Responsive-Vertrag und Accessibility-Gates sind vollständig.');