Das WebKit im Schaufenster
Zuletzt geändert: 04.08.2026 06:54

Übersicht

Das WebKit im Schaufenster #

Eine Oberfläche für ein EULANDA-Plugin zu bauen fühlt sich an wie HTML schreiben - nur dass am Ende kein nacktes Formular steht, sondern ein fertiger, themenfähiger, mehrsprachiger Dialog. Das WebKit aus EulandaXtools liefert dafür einen ganzen Satz eul-*-Bausteine: Sie kennen Hell- und Dunkelmodus, sie sind lokalisiert, sie sehen über alle Plugins gleich aus - und man bindet sie ein, indem man ein Tag schreibt. Kein Framework-Projekt, kein Build-Schritt, keine Node-Toolchain.

Dieses Kapitel ist die Einladung: ein Rundgang durch die Bausteine, jeweils mit einem Blick in ein echtes Fenster und der einen Zeile, die ihn erzeugt. Am schnellsten erlebt man das live - Show-EulWebUiStyleguide öffnet genau diese Galerie als Dialog samt Theme-Umschalter; alle Abbildungen hier stammen daraus.

Schnellstart: installieren und ausprobieren #

Man braucht nur Windows mit Windows PowerShell - kein Visual Studio, kein Node.

EulandaXtools bereits installiert (mindestens 1.0.141)? Dann startet der Rundgang mit zwei Zeilen in einem PowerShell-Fenster:

Import-Module EulandaXtools
Show-EulWebUiStyleguide

(Sollte PowerShell melden, dass Skripte nicht erlaubt sind, dieser Sitzung einmalig erlauben: Set-ExecutionPolicy -Scope Process RemoteSigned -Force.)

Noch nicht installiert oder soll aktualisiert werden? Das folgende Skript prüft die Online-Version, holt EulandaXtools bei Bedarf und öffnet danach den Style Guide. Es ist eingeklappt - direkt kopieren oder zum Lesen aufklappen:

Installations-/Update-Skript anzeigen
# EulandaXtools bei Bedarf aktualisieren und den Style Guide öffnen.
$ProgressPreference = 'SilentlyContinue'
Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned -Force
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

$module = 'EulandaXtools'
$base   = "https://files.eulanda.eu/$($module.ToLower())"

# Modulpfad im (ggf. nach OneDrive umgelenkten) Benutzerprofil - Windows PowerShell 5.1
$modulesRoot = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'WindowsPowerShell\Modules'
$installRoot = Join-Path $modulesRoot $module

# Online-Version von files.eulanda.eu (IIS) lesen
$online = (Invoke-RestMethod "$base/version.txt") -replace '[^\d.]', ''
Write-Host "Online (files.eulanda.eu): $module $online" -ForegroundColor Cyan

# Höchste lokal installierte Version ermitteln
$installed = $null
if (Test-Path $installRoot) {
  $installed = Get-ChildItem $installRoot -Directory |
    Where-Object { $_.Name -as [version] } |
    Sort-Object { [version]$_.Name } -Descending |
    Select-Object -First 1 -ExpandProperty Name
}
if ($installed) { Write-Host "Lokal installiert:         $module $installed" -ForegroundColor Cyan } else { Write-Host "Lokal installiert:         (noch nicht vorhanden)" -ForegroundColor Cyan }

if (-not $installed -or [version]$online -gt [version]$installed) {
  Write-Host "Aktualisiere auf $online ..." -ForegroundColor Yellow
  $tmp = Join-Path $modulesRoot ".$module-download"
  $ext = Join-Path $tmp 'extracted'
  if (Test-Path $tmp) { Remove-Item $tmp -Recurse -Force }
  New-Item -ItemType Directory -Path $ext -Force | Out-Null
  $zip = Join-Path $tmp "$module.zip"
  Invoke-WebRequest "$base/$($module.ToLower()).zip" -OutFile $zip
  Expand-Archive $zip -DestinationPath $ext -Force

  # Das Manifest (.psd1) ist die verbindliche Quelle für die Versionsnummer
  $psd1 = Get-ChildItem $ext -Recurse -Filter "$module.psd1" | Select-Object -First 1
  if (-not $psd1) { throw "$module.psd1 im Download nicht gefunden." }
  $version = (Import-PowerShellDataFile $psd1.FullName).ModuleVersion

  $target = Join-Path $installRoot $version
  if (Test-Path $target) { Remove-Item $target -Recurse -Force }
  New-Item -ItemType Directory -Path $target -Force | Out-Null
  Copy-Item (Join-Path $psd1.Directory.FullName '*') $target -Recurse -Force
  Get-ChildItem $target -Recurse -File | Unblock-File -ErrorAction SilentlyContinue   # Mark of the Web entfernen
  Remove-Item $tmp -Recurse -Force
  Write-Host "Installiert: $module $version" -ForegroundColor Green
} else {
  Write-Host "Bereits aktuell - kein Download." -ForegroundColor Green
}

Write-Host "Starte den Style Guide ..." -ForegroundColor Cyan
Import-Module $module -Force
Show-EulWebUiStyleguide

Was das Skript tut:

  • Versionsabgleich: Es liest die Online-Version aus version.txt und vergleicht sie mit der höchsten lokal installierten - heruntergeladen wird nur, wenn das Web wirklich neuer ist.
  • Sichere Versionsbestimmung: Der Download landet zuerst in einem Temp-Ordner; die maßgebliche Versionsnummer kommt aus dem Manifest (.psd1), nicht aus dem Dateinamen. Erst damit entsteht der endgültige Zielordner ...\Modules\EulandaXtools\<Version>\. Genau so macht es auch ErpXe beim Nachladen.
  • OneDrive-fest: Der Modulpfad wird über den Windows-Known-Folder „Eigene Dokumente" aufgelöst - also auch bei nach OneDrive umgeleitetem Dokumente-Ordner.
  • Sofort lauffähig: Set-ExecutionPolicy -Scope Process RemoteSigned erlaubt das Laden des EV-signierten Moduls in dieser Sitzung; Unblock-File nimmt den frisch entpackten Dateien das „Mark of the Web". Danach Import-Module -Force - und der Beispieldialog geht auf.

Ein manuelles Freischalten vorab ist nicht nötig. Das Skript wird in ein PowerShell-Fenster eingefügt (interaktiv ausgeführt, nicht als .ps1-Datei gestartet) - die Execution Policy blockt nur Skript-/Moduldateien, nicht eingefügte Befehle. Deshalb läuft die erste Zeile auch unter der Standardrichtlinie „Restricted", ohne Adminrechte, und Scope Process betrifft nur diese eine Sitzung. Wer das Skript stattdessen als .ps1 speichert und startet, ruft es mit powershell -ExecutionPolicy Bypass -File <datei>.ps1 auf. (Einzige echte Ausnahme: eine per Gruppenrichtlinie zentral erzwungene Policy.)

Ab jetzt steht Show-EulWebUiStyleguide jederzeit bereit und alle eul-*-Bausteine sind einsatzbereit. Und damit der Rundgang:

Buttons #

Der Anfang ist unspektakulär und genau deshalb angenehm: Ein eul-button ist ein Button, der schon richtig aussieht. Fünf Varianten transportieren die Bedeutung (primary, secondary, ghost, success, danger), drei Größen die Gewichtung - mehr muss man nicht entscheiden.

Buttons in allen Varianten und Größen

Varianten und Größen - jede Farbe trägt eine Bedeutung.

<eul-button variant="success">Speichern</eul-button>
<eul-button variant="ghost"><eul-icon name="settings"></eul-icon></eul-button>

Formularfelder #

Text, E-Mail, Zahl, Auswahl, Checkbox, Radio, Schalter - die üblichen Verdächtigen, aber aus einem Guss. Das Sahnehäubchen ist eul-tag-input: ein Feld mit Autocomplete, das Vorschläge filtert und als Chips übernimmt - ideal, wenn der Anwender Spalten oder Schlagworte zusammenstellt.

Eingabefelder, Schalter und Tag-Input

Von Text bis Schalter, plus das Tag-Input mit Vorschlagsliste.

<eul-input type="email" placeholder="name@firma.de"></eul-input>
<eul-switch checked>Aktiv</eul-switch>

Form-Layout #

Formulare sollen ohne Handarbeit ordentlich aussehen. eul-form mit eul-form-row legt Label und Eingabe ab 560 px Breite nebeneinander und stapelt sie darunter - per Container-Query, nicht per Fensterbreite. Der graue Hilfetext unter einem Feld ist eine einzige Zeile.

Responsives Label-Eingabe-Layout

Label und Eingabe sortieren sich je nach Platz selbst.

<eul-form>
  <div class="eul-form-row">
    <label>E-Mail</label>
    <div>
      <eul-input type="email"></eul-input>
      <div class="eul-form-help">Wird für Benachrichtigungen genutzt.</div>
    </div>
  </div>
</eul-form>

Datepicker #

Datum, Datum mit Uhrzeit, Zeitraum - eul-datepicker deckt alle drei ab, mit deutschem Format und Kalender-Popup. Ein Attribut entscheidet, welcher Typ.

Datum, Datum mit Zeit und Zeitraum

Drei Picker-Typen aus einer Komponente.

<eul-datepicker type="range" placeholder="von - bis"></eul-datepicker>

HTML-Editor #

Wo der Anwender formatierten Text braucht - ein Anschreiben, eine Mail-Vorlage - liefert eul-htmleditor einen WYSIWYG-Editor mit drei Toolbar-Stufen. Wichtig fürs Backend: el.mailHtml gibt sauberes, auf das EULANDA-Subset reduziertes HTML zurück, kein Editor-Wirrwarr.

WYSIWYG-Editor mit Mail-Toolbar

Formatierter Text, der als sauberes HTML zurückkommt.

<eul-htmleditor toolbar="mail" height="160"></eul-htmleditor>
const html = document.querySelector('eul-htmleditor').mailHtml;

Cards / Tönungen #

eul-card ist der Rahmen für alles. Über tone bekommt eine Karte eine Bedeutung (info, success, warning, danger), über elevation Tiefe, über level eine saubere Verschachtelung - aus drei Attributen entsteht eine ganze Gestaltungssprache.

Tönungen, Schattenstufen und Ebenen

Variant, Tone, Elevation und verschachtelte Level.

<eul-card tone="warning">Bitte prüfen</eul-card>

Tabs / Accordion #

Zwei klassische Gliederungen, beide rein über CSS-Klassen, ohne eine Zeile JavaScript: eul-tabs für Reiter nebeneinander, eul-accordion für aufklappbare Abschnitte untereinander. Je das erste Element trägt selected beziehungsweise open.

Reiter und Aufklapp-Bereiche

Tabs oben, Accordion darunter - rein deklarativ.

<eul-tabs>
  <div class="eul-tabs-list">
    <button class="eul-tab" selected>Allgemein</button>
    <button class="eul-tab">Optionen</button>
  </div>
  <div class="eul-tab-panel" selected>...</div>
  <div class="eul-tab-panel">...</div>
</eul-tabs>

Feedback #

Rückmeldung an den Anwender ist Einzeiler-Sache. EulUI.toast wirft eine kurze Meldung ein (vier Varianten), EulUI.alert und EulUI.confirm liefern Hinweis und Rückfrage als Promise. Dazu eul-spinner und eul-progress für alles, was dauert.

Toasts, Alert, Confirm, Spinner und Fortschritt

Kurzmeldungen, Dialoge und Ladeanzeigen.

EulUI.toast('Gespeichert', { variant: 'success' });
if (await EulUI.confirm('Wirklich löschen?')) { /* ... */ }

Wizard #

Mehrstufige Abläufe - Import, Einrichtung, Migration - führt eul-wizard Schritt für Schritt. Jede Seite trägt ihren Titel im data-title, die Schritt-Navigation entsteht von selbst.

Schritt-für-Schritt-Assistent

Geführter Ablauf mit Fortschrittsleiste.

<eul-wizard>
  <div class="eul-wizard-content">
    <div class="eul-wizard-page" data-title="Start" active>...</div>
    <div class="eul-wizard-page" data-title="Prüfen">...</div>
  </div>
</eul-wizard>

Tree / Listview #

Hierarchien zeigt eul-tree - mit Checkboxen, Einfachauswahl und einer Echtzeitsuche, die den Baum beim Tippen filtert. Für flache Listen gibt es eul-listview. Daten kommen als Property, die Auswahl liest man über .checkedIds oder .selectedId zurück.

Baum mit Echtzeitsuche, daneben eine Liste

Baum mit Sofortfilter und Mehrfachauswahl, plus Liste.

<eul-tree id="t" checkboxes searchable></eul-tree>
document.getElementById('t').data = [
  { id: 'a', label: 'Eigenschaften', children: [{ id: 'farbe', label: 'Farbe' }] }
];

Grid #

Die Tabelle, die im echten Plugin am häufigsten gebraucht wird. eul-grid bringt Suche, Sortierung und Pagination schon als Attribute mit; Spalten und Zeilen setzt man über .columns und .data. Sind keine Daten da, erscheint automatisch ein Empty-Overlay.

Tabelle mit Suche, Sortierung und Seitenzahlen

Volltextsuche, Spaltensortierung und Pagination ohne Zutun.

<eul-grid search sort pagination></eul-grid>
const g = document.querySelector('eul-grid');
g.columns = ['ArtNr', 'Bezeichnung', 'Bestand', 'Preis'];
g.data = rows;

Attribute im Überblick #

Alles opt-in: ein Grid ohne Attribute ist eine schlichte Tabelle.

AttributWirkung
searchVolltextsuche über alle Spalten
sortSpaltensortierung
paginationSeitenweise Anzeige (10 Zeilen)
selectableZeilenauswahl mit Marker
auto-selectwählt nach dem Laden die erste Zeile
commandsKommandozeile (neu, bearbeiten, löschen, aktualisieren)
exportExport nach CSV, Excel und Zwischenablage
auto-filterTrichter an Spalten mit wenigen verschiedenen Werten
column-setupSpalten ein-/ausblenden, sortieren, Breiten merken
fixed-layoutTabelle bleibt in Containerbreite, Text endet in Ellipse

Zeilenauswahl gegen Mehrfachauswahl #

Zwei verschiedene Dinge, die man leicht verwechselt - und deren Vermischung teuer wird:

Zeilenauswahl (selectable) beantwortet wo bin ich. Ein Klick markiert die Zeile mit einem Strich, damit sichtbar ist, welchen Datensatz die Widgets daneben gerade zeigen. Sie folgt auch den Pfeiltasten und dem Mausrad - beides bewegt die Auswahl um eine Zeile. Gemeldet wird das über eul-grid-row-click.

Mehrfachauswahl (Spalten-Eigenschaft check: true) beantwortet was habe ich ausgewählt. Die Spalte zeigt eine echte Checkbox: per Tab erreichbar, mit LEER schaltbar, und ein Klick darauf verschiebt die Zeilenauswahl nicht.

g.columns = [
  'id',                                              // Schlüsselspalte
  { name: 'Auswahl', check: true, checkAll: true },   // echte Checkboxen
  'Tabelle'
];
g.data = rows;
g.checkedKeys = rows.map((r) => r[0]);               // alles vorauswählen

g.addEventListener('eul-grid-check-change', (e) => {
  // e.detail = { key, checked, keys, cells, row }
  // key ist null, wenn der Kopf-Haken alle auf einmal geschaltet hat
  console.log(e.detail.keys);
});

checkedKeys führt die Auswahl über den Schlüssel der ersten Zelle - genau wie die Zeilenauswahl. Damit überlebt sie Sortierung, Suche und Seitenwechsel. checkAll: true setzt zusätzlich einen Alle/Keine-Haken in den Spaltenkopf; er bezieht sich auf die Zeilen, die nach Suche und Filter gerade gelten, und zeigt eine teilweise Auswahl als unbestimmt an.

Haken nie über eul-grid-row-click schalten. Es liegt nahe, eine Häkchen-Spalte als Text zu führen und im Zeilenklick umzuschalten. Das geht schief: dasselbe Ereignis feuert auch bei Pfeiltasten und Mausrad. Wer scrollt, schaltet dann Haken um, ohne es zu wollen - und mit der Tastatur ist ein Haken gar nicht erreichbar, ohne ihn im selben Moment zu setzen. Dafür gibt es check: true.

Spalten als Objekt #

Statt eines Namens darf jede Spalte ein Objekt sein:

EigenschaftWirkung
nameÜberschrift
typeint, number, money - rechtsbündig und formatiert
filter: trueTrichter im Kopf, Werte distinct aus den geladenen Daten
sum: trueSummenzeile unter der Spalte
hidden: trueSpalte bleibt im DOM (Schlüssel!), wird nur nicht gezeigt
check / checkAllAuswahl-Spalte, siehe oben

hidden ist bewusst kein Entfernen: die Schlüsselspalte muss im DOM bleiben, sonst hat die Zeilenauswahl keinen Schlüssel mehr.

Export nach CSV, Excel und Zwischenablage #

export setzt drei Knöpfe in den Tabellenkopf. Alles läuft im Browser, ohne Server-Roundtrip und ohne zusätzliche Bibliothek:

ZielFormat
CSVSemikolon-getrennt, mit BOM (Excel öffnet Umlaute richtig)
Excelechtes .xlsx, clientseitig gepackt
ZwischenablageTSV - passt direkt in eine Tabellenkalkulation

Exportiert wird, was man sieht. Nicht der Rohbestand: Suche und Spaltenfilter wirken auch auf den Export, und ausgeblendete Spalten bleiben draussen - hidden: true, per CSS versteckte (die Schlüsselspalte) und alles mit export: false. Wer eingrenzt und dann exportiert, bekommt genau seine Auswahl.

<eul-grid export export-name="offene-posten"></eul-grid>
g.columns = [
  { name: 'id', hidden: true },
  'Beleg',
  { name: 'Intern', export: false },   // im Grid sichtbar, nicht im Export
  { name: 'Betrag', type: 'money' }
];
g.addEventListener('eul-grid-export', (e) => {
  console.log(e.detail.format, e.detail.rows.length);   // csv | xlsx | copy
});

Der Dateiname ist <export-name>_JJJJ-MM-TT; ohne export-name dient die id des Grids als Basis. Das Ereignis eul-grid-export meldet Format und Zeilen - nützlich fürs Protokoll, wenn ein Anwender Daten aus dem Haus trägt.

Sichtzustand über einen Detail-Sprung retten #

Wer aus der Liste in eine Detailmaske springt und zurückkehrt, will nicht auf Seite 1 landen:

const state = g.viewState;      // vor dem Wechsel
// ... Detail bearbeiten, Daten neu laden ...
g.restoreViewState(state);      // Seite und markierte Zeile sind zurück

Charts #

Zahlen werden Bild: eul-chart umhüllt Chart.js für Balken, Torte, Linie und Doughnut. Ein Attribut wählt den Typ, .data nimmt das vertraute Chart.js-Format.

Balken- und Tortendiagramm

Aus einem Datenobjekt wird ein fertiges Diagramm.

<eul-chart type="bar" height="220"></eul-chart>
document.querySelector('eul-chart').data = {
  labels: ['Jan', 'Feb', 'Mär'],
  datasets: [{ label: 'Umsatz', data: [42, 48, 55] }]
};

Icons #

40 randscharfe Lucide-Icons als Inline-SVG, skalierbar und einfärbbar. size bestimmt die Größe, color die Bedeutung - und im Button wird daraus mit einem Tag ein Icon-Button.

Das Lucide-Set in Größen und Farben

Ein konsistentes Icon-Set, das überall passt.

<eul-icon name="trash-2" color="danger"></eul-icon>
<eul-button><eul-icon name="save"></eul-icon> Speichern</eul-button>

EULANDA-Icons #

Eigene Bilddateien - Logos, Symbole - liegen im Modul-Ordner lib/img/ und sind im Dialog als eulimg/... erreichbar; den Virtual-Host dafür richtet Show-EulWebView2Dialog automatisch ein. eul-image zeigt sie scharf in jeder Größe.

Bilder aus dem Modul über den eulimg-Virtual-Host

Modul-eigene Bilder, ohne Pfad-Gefummel.

<eul-image src="eulimg/eulanda-logo.png" width="48"></eul-image>

App-Layout #

Und schließlich der Rahmen, der alles zusammenhält: eul-app mit Kopfleiste und Body, darin eul-sidebar (Navigationsleiste links, Seiten rechts) oder eul-ribbon (Reiter oben). Der Seitenwechsel läuft über ein data-page-Attribut - genau dieses Muster trägt auch der Style Guide selbst.

Sidebar- und Ribbon-Layout

Der Plugin-Rahmen als Sidebar oder Ribbon.

<eul-sidebar>
  <nav slot="nav">
    <a data-page="start" active>Start</a>
    <a data-page="export">Export</a>
  </nav>
  <eul-page id="start" active>...</eul-page>
  <eul-page id="export">...</eul-page>
</eul-sidebar>

Gruppen in der Navigationsleiste #

Ab einer gewissen Zahl von Menüpunkten wird die flache Liste unübersichtlich. Dann bekommt die Navigation Gruppen wie die Outlook-Bar der EULANDA-Klassik: Überschrift, auf- und zuklappbar, der Zustand bleibt je Gerät gemerkt.

<nav slot="nav">
  <div class="eul-nav-group" data-group="master" open>
    <button type="button" class="eul-nav-group-head">
      <span>Stammdaten</span><span class="eul-nav-caret"></span>
    </button>
    <div class="eul-nav-group-items">
      <a data-page="addresses">Adressen</a>
      <a data-page="articles">Artikel</a>
    </div>
  </div>
</nav>

Verdrahtet wird das einmal beim Aufbau mit EulUI.wireNavGroups(nav, 'meinPlugin.navGroups') - der zweite Wert ist der Schlüssel, unter dem der Auf-/Zu-Zustand liegt. Der Aufruf arbeitet delegiert: Gruppen, die später dazukommen (etwa aus einem Plugin-Manifest), brauchen kein erneutes Verdrahten, nur ein EulUI.applyNavGroupState(nav, key), damit auch für sie der gemerkte Zustand gilt. Ohne open im Markup startet eine Gruppe zugeklappt - richtig für Selten-Gebrauchtes wie eine System-Gruppe.

Am Handy wird daraus eine Schublade #

Unter 640px Fensterbreite hat eine Navigation mit Gruppen keinen Platz mehr neben dem Inhalt. Sie wird dann zur Schublade: aus dem Bild geschoben, ein Knopf mit drei Balken links in der Kopfleiste holt sie über den Inhalt, dahinter dunkelt eine Fläche ab. Geschlossen wird sie durch die Auswahl eines Ziels, einen Tipp auf die Abdunklung, ESC oder sobald das Fenster wieder breiter wird.

Navigation als Schublade am Handy

Dieselbe Navigation am Telefon: Knopf in der Kopfleiste, Gruppen zugeklappt, eine aufgeklappt.

Dafür ist nichts zu tun - kein Markup, kein Aufruf, kein eigenes CSS. Zwei Dinge sollte man trotzdem wissen:

Nur mit Gruppen. Eine flache Navigation (nur Verweise) bleibt der waagerechte Streifen von früher; geprüft wird das am Element selbst. Bestehende Dialoge ändern ihr Verhalten also nicht.

In der Schublade starten alle Gruppen zugeklappt, auch die mit open - sonst wäre das Menü zwanzig Zeilen lang. Was man dort aufklappt, gilt für den Moment und wird nicht gespeichert: ein Tipp unterwegs soll die Gliederung am Arbeitsplatz nicht umschreiben.

Eigene Klick-Handler auf den Menü-Verweisen stören nicht, auch nicht mit stopPropagation - die Schublade hört in der Aufnahme-Phase mit und schliesst zuverlässig.


All das kommt aus einem einzigen Bundle, das Show-EulWebView2Dialog automatisch einsetzt - Design-Tokens, Komponenten, Sprache. Man schreibt die eul-*-Tags, der Rest ist erledigt. Wer loslegen will: WebView2 in PowerShell zeigt das Gerüst, Dialog-Aufbau und Bausteine die Struktur dahinter.