Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94d898e445 |
@@ -34,9 +34,6 @@ 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
|
||||
|
||||
@@ -147,14 +144,6 @@ 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"
|
||||
@@ -170,19 +159,7 @@ jobs:
|
||||
|
||||
- name: Vollständigen Vendoo-Releasevertrag unter Windows ausführen
|
||||
shell: pwsh
|
||||
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
|
||||
run: npm run verify:all
|
||||
|
||||
package-after-acceptance:
|
||||
name: Updater-3.4-Paket nach Abnahme
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
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')); } }"
|
||||
@@ -1,70 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,144 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,127 +0,0 @@
|
||||
# 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
|
||||
@@ -1,97 +0,0 @@
|
||||
# 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
|
||||
@@ -1,172 +0,0 @@
|
||||
# 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
|
||||
Generated
+754
-604
File diff suppressed because it is too large
Load Diff
+7
-26
@@ -22,26 +22,7 @@
|
||||
"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: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: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: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",
|
||||
@@ -68,15 +49,15 @@
|
||||
"verify:release-manifest": "node tools/verify-release-manifest-staging-3.0.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@anthropic-ai/sdk": "^0.111.0",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"helmet": "^8.2.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"helmet": "^8.3.0",
|
||||
"multer": "^2.2.0",
|
||||
"nodemailer": "^9.0.3",
|
||||
"openai": "^4.77.0",
|
||||
"sharp": "^0.33.5"
|
||||
"openai": "^6.46.0",
|
||||
"sharp": "^0.35.3"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,617 +0,0 @@
|
||||
(() => {
|
||||
'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 });
|
||||
})();
|
||||
@@ -1,497 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
(() => {
|
||||
'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 +0,0 @@
|
||||
.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}}
|
||||
@@ -1,453 +0,0 @@
|
||||
(() => {
|
||||
'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();
|
||||
})();
|
||||
@@ -70,175 +70,7 @@ 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();
|
||||
});
|
||||
@@ -255,4 +87,4 @@ window.VendooTheme = Object.freeze({
|
||||
return applyState({ ...current, theme_id: BUILTIN.has(themeId) ? themeId : 'system', custom_theme: null }, { persist });
|
||||
},
|
||||
supported: Object.freeze([...BUILTIN]),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,537 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -1,478 +0,0 @@
|
||||
(() => {
|
||||
'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();
|
||||
}
|
||||
})();
|
||||
@@ -1,459 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,387 +0,0 @@
|
||||
(() => {
|
||||
'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();
|
||||
})();
|
||||
@@ -1,373 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -1,456 +0,0 @@
|
||||
(() => {
|
||||
'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();
|
||||
})();
|
||||
@@ -1,523 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
(() => {
|
||||
'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();
|
||||
}
|
||||
})();
|
||||
@@ -1,533 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
(() => {
|
||||
'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 A–Z'], ['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();
|
||||
})();
|
||||
@@ -1,695 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -1,406 +0,0 @@
|
||||
(() => {
|
||||
'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();
|
||||
}
|
||||
})();
|
||||
@@ -1,26 +0,0 @@
|
||||
#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}
|
||||
@@ -1,268 +0,0 @@
|
||||
(() => {
|
||||
'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();
|
||||
})();
|
||||
@@ -1,84 +0,0 @@
|
||||
(() => {
|
||||
'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' },
|
||||
}));
|
||||
})();
|
||||
@@ -1,595 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,370 +0,0 @@
|
||||
(() => {
|
||||
'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();
|
||||
}
|
||||
})();
|
||||
@@ -1,438 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
(() => {
|
||||
'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();
|
||||
}
|
||||
})();
|
||||
@@ -1,345 +0,0 @@
|
||||
(() => {
|
||||
'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 });
|
||||
})();
|
||||
@@ -1,39 +0,0 @@
|
||||
#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}
|
||||
@@ -1,205 +0,0 @@
|
||||
(() => {
|
||||
'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 });
|
||||
})();
|
||||
@@ -1,485 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
(() => {
|
||||
'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();
|
||||
})();
|
||||
@@ -1,375 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
(() => {
|
||||
'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();
|
||||
})();
|
||||
@@ -1,360 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
(() => {
|
||||
'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();
|
||||
}
|
||||
})();
|
||||
+6
-261
@@ -61,8 +61,8 @@
|
||||
},
|
||||
{
|
||||
"path": ".github/workflows/ci.yml",
|
||||
"sha256": "f009390f9672508c3de9e5483bb1acc6da9ea4845a51159e4cc24821882550f4",
|
||||
"size": 8710
|
||||
"sha256": "c0e80dbafb1d1491b851eb002dc323e52ea89e7e0456dde034f3303a8f4751c2",
|
||||
"size": 8173
|
||||
},
|
||||
{
|
||||
"path": ".github/workflows/release.yml",
|
||||
@@ -769,16 +769,6 @@
|
||||
"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",
|
||||
@@ -2051,8 +2041,8 @@
|
||||
},
|
||||
{
|
||||
"path": "package.json",
|
||||
"sha256": "6931aecd4f018bd6df1f256e667ffd84d5d386837cb7d4da264e4e78d5ea59c5",
|
||||
"size": 6818
|
||||
"sha256": "cd21451f91312c33bc9bd5b808103c3428854587cc8e9b5ba3b2908085043344",
|
||||
"size": 4759
|
||||
},
|
||||
{
|
||||
"path": "platforms/ebay-de.mjs",
|
||||
@@ -2074,16 +2064,6 @@
|
||||
"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",
|
||||
@@ -2104,16 +2084,6 @@
|
||||
"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",
|
||||
@@ -2274,16 +2244,6 @@
|
||||
"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",
|
||||
@@ -2306,8 +2266,8 @@
|
||||
},
|
||||
{
|
||||
"path": "public/design-system/theme-runtime.js",
|
||||
"sha256": "7ce328987c17873495db28d8da09bbeb3e1e5e6f966a6d948a5a45a05656d41e",
|
||||
"size": 10940
|
||||
"sha256": "308f1c0e2ed1de34aa11545a1a660aab02631e422d6c6945e3203d11666784fc",
|
||||
"size": 4004
|
||||
},
|
||||
{
|
||||
"path": "public/design-system/tokens/source.json",
|
||||
@@ -2324,61 +2284,11 @@
|
||||
"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",
|
||||
@@ -2389,16 +2299,6 @@
|
||||
"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",
|
||||
@@ -2419,16 +2319,6 @@
|
||||
"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",
|
||||
@@ -2454,71 +2344,11 @@
|
||||
"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",
|
||||
@@ -2699,11 +2529,6 @@
|
||||
"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",
|
||||
@@ -2774,16 +2599,6 @@
|
||||
"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",
|
||||
@@ -2934,16 +2749,6 @@
|
||||
"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",
|
||||
@@ -2964,11 +2769,6 @@
|
||||
"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",
|
||||
@@ -3029,11 +2829,6 @@
|
||||
"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",
|
||||
@@ -3064,21 +2859,6 @@
|
||||
"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",
|
||||
@@ -3244,16 +3024,6 @@
|
||||
"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",
|
||||
@@ -3354,11 +3124,6 @@
|
||||
"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",
|
||||
@@ -3394,11 +3159,6 @@
|
||||
"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",
|
||||
@@ -3439,21 +3199,11 @@
|
||||
"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",
|
||||
@@ -3474,11 +3224,6 @@
|
||||
"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",
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
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;
|
||||
@@ -1,56 +0,0 @@
|
||||
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.');
|
||||
@@ -1,174 +0,0 @@
|
||||
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.');
|
||||
@@ -1,31 +0,0 @@
|
||||
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.');
|
||||
@@ -1,102 +0,0 @@
|
||||
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.');
|
||||
@@ -1,65 +0,0 @@
|
||||
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.');
|
||||
@@ -1,33 +0,0 @@
|
||||
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.');
|
||||
@@ -1,91 +0,0 @@
|
||||
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.');
|
||||
@@ -1,104 +0,0 @@
|
||||
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.');
|
||||
@@ -1,91 +0,0 @@
|
||||
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.');
|
||||
@@ -1,35 +0,0 @@
|
||||
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.');
|
||||
@@ -1,138 +0,0 @@
|
||||
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.');
|
||||
@@ -1,73 +0,0 @@
|
||||
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.');
|
||||
@@ -1,112 +0,0 @@
|
||||
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.');
|
||||
@@ -1,250 +0,0 @@
|
||||
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.');
|
||||
@@ -1,69 +0,0 @@
|
||||
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.');
|
||||
@@ -1,111 +0,0 @@
|
||||
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.');
|
||||
@@ -1,112 +0,0 @@
|
||||
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.');
|
||||
@@ -1,218 +0,0 @@
|
||||
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.');
|
||||
@@ -1,46 +0,0 @@
|
||||
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.');
|
||||
Reference in New Issue
Block a user