major
#29661
TL Views: Prototyp "TopLogic UI on MUI" – Adaptermodul mit Material UI für die ersetzbaren Komponenten
Teil von #29650 (TopLogic UI on MUI), Richtung 2: Prototyp.
Ziel
Eine Anwendung, die mit der React-Oberfläche (.view.xml) gebaut ist, lässt sich mit Material UI (MUI) im MUI-Theme des Kunden darstellen – über den Mechanismus und den Leitfaden docs/faq/customer-component-library.md aus #29643. Die unveränderten Views von tl-demo-react zeigen das.
Umfang
Modul
- Ein React-Modul nach docs/faq/new-react-module.md, das @mui/material, @mui/x-date-pickers und @emotion/* gegen das React aus tl-react-bridge bündelt (keine zweite React-Instanz).
- Nur MIT-lizenzierte MUI-Pakete.
- Zuschaltung in tl-demo-react über das Maven-Profil mui.
Wurzel-Wrapper
registerRootWrapper mit ThemeProvider, LocalizationProvider (dayjs, Locale des Benutzers) und emotion-CacheProvider, der die MUI-Styles in einer festen Reihenfolge zu den TL-Stylesheets einfügt.
Adapter
Ein Adapter je Zustand des Vertrags aus #29643 bzw. seiner Erweiterungen.
Prüfpunkte
- Fokusfalle, Escape und Außenklick von MUI-Dialogen und -Popovern im Zusammenspiel mit dem TL-Fenstermanager und den Tastatur-Scopes.
- Tastaturkürzel der Schaltflächen (useKeyboardBinding).
- Fill-Vertrag: ersetzte Reiter und Fenster füllen ihren Container wie die TL-Komponenten.
- Keine Konsolenfehler; Speichern/Aktionen funktionieren unverändert.
Ergebnis
Neben dem Modul eine Liste der Stellen, an denen der Zustandsvertrag oder die Bridge für eine vollständige Ersetzung nicht ausreicht, als Grundlage für weitere Tickets.
Lösung
Erweiterung des Zustandsvertrags (`com.top_logic.layout.react`)
state.proto enthält Zustandsnachrichten für die Komponenten mit großem optischen Anteil: FormFieldState (TLFormField), TextState (TLText), CardState (TLCard), AppBarState (TLAppBar), BreadcrumbState (TLBreadcrumb), ProgressState (TLProgress), SliderState (TLSlider); außerdem ist TLChoiceGroup (Radio-/Checkbox-Gruppe aus #29654) als ersetzbare Komponente von DropdownSelectState aufgeführt. Die Controls veröffentlichen ihre Zustandsschlüssel über die generierten Konstanten, TestControlStateSchema prüft jeden Schlüssel und jede Aufzählung gegen die Nachricht. (TLAppBar las einen Schlüssel color, den der Server nie sendet; er und die zugehörige CSS-Regel .tlAppBar--surface entfallen.)
Erweiterungspunkte der Bridge
Damit eine Ersetzung sich in Formularen, Toolbars, Menüs, App-Bars und Fenstern wie die TL-Komponente verhält, bietet tl-react-bridge zusätzlich an:
- jsx, jsxs, Fragment – die automatische JSX-Laufzeit der gemeinsamen React-Instanz. Die Shims der React-Module (react-jsx-runtime-shim.ts in wysiwyg, chartjs, mui) exportieren sie nur noch weiter. Bisher bildeten sie jsx auf React.createElement ab; das verwirft bei vorkompiliertem Bibliothekscode die Kinder jedes Elements mit Schlüssel.
- FormLayoutContext / useFormLayout() – Nur-Lese-Zustand und aufgelöste Beschriftungsposition des umgebenden Formulars (bisher intern in den Controls).
- ButtonDefaults / useButtonDefaults() / menuItemProps() – die Vorgaben eines Containers für seine Schaltflächen (Ghost in der App-Bar, Secondary im Fensterfuß, kompakte Toolbar, Menüeintrag mit role=menuitem und Roving-Tabindex im Menü) (bisher intern in den Controls).
Behobene Fehler der TL-Oberfläche
- Ein Formular meldete seinem Formular-Layout nie, dass es nur lesend ist: Pflichtmarkierungen und Hilfe-Schaltflächen erschienen auch in der Ansicht. FormControl veröffentlicht jetzt readOnly = nicht im Bearbeitungsmodus (FormLayoutEditModeBinding).
- Ein Fenster mit hohem Inhalt sprang beim Verschieben nach oben und wuchs auf die volle Fensterhöhe, weil die 80vh-Grenze beim ersten Verschieben entfiel. Die Grenze gilt jetzt, bis der Benutzer die Größe ändert.
Modul `com.top_logic.layout.react.mui` (`tl-layout-react-mui`)
Das Modul ist für den Produktiveinsatz bestimmt: eine Anwendung, die im MUI-Design erscheinen soll, bindet es ein und installiert es mit dem Theme des Kunden.
- Aufbau: kein Java-Code, Bündel als ClientResources, veröffentlicht in der Import-Map unter dem Bezeichner tl-react-mui, React-Shims für die gemeinsame React-Instanz. Abhängigkeiten nur MIT: @mui/material 9, @mui/x-date-pickers 9, @emotion/react, @emotion/styled, @emotion/cache, dayjs.
- Das Laden des Bündels verändert die Seite nicht. Die Anwendung ruft einmal installMui({ theme, replace }) auf: theme sind die createTheme-Optionen des Kunden (auch mit Funktionen), replace ist 'all' oder eine Auswahl der 25 Komponentennamen (COMPONENT_NAMES). Ein unbekannter Name oder ein zweiter Aufruf ist ein Fehler.
- Das Bündel exportiert außerdem @mui/material und @mui/x-date-pickers vollständig weiter. Eigene MUI-Komponenten des Kunden importieren MUI von dort und teilen so Theme, emotion-Cache und Datumslokalisierung mit den Adaptern; es gibt genau ein MUI auf der Seite. Bündelgröße ca. 1,23 MB (306 kB gzip).
- Wurzel-Wrapper ohne eigenes DOM-Element (sonst bricht der Fill-Vertrag): emotion-CacheProvider (MUI-Styles am Ende von <head>, hinter den TL-Stylesheets), ThemeProvider mit dem Theme des Kunden in der Sprache der Seite (de/en, inkl. Texte der Datumsauswahl), LocalizationProvider (dayjs). Kein CssBaseline/`ScopedCssBaseline`: der globale Reset kollidiert mit den TL-Stylesheets, ScopedCssBaseline brächte ein DOM-Element.
- Hell und Dunkel: Die Farbschemata eines MUI-Themes folgen dem Modus des gewählten UI-Themes (data-tl-mode an <html>, gesetzt vom UIThemeService), wie die TL-Komponenten – auch beim UI-Theme "System" und ohne Neuladen der Seite. Das Theme wird mit colorSchemeSelector: '[data-tl-mode="%s"]' erzeugt (übrige cssVariables-Optionen des Kunden bleiben); MUI schreibt keinen eigenen Modus an <html> und nichts in den localStorage.
- Adapter für alle 25 ersetzbaren Komponenten: TLButton → Button/`IconButton`/Menüeintrag, TLToggleButton → ToggleButton, TLCheckbox → Checkbox/`Switch`, TLTextInput/`TLPasswordInput`/`TLNumberInput` → TextField (Debounce, Senden beim Verlassen, Enter wie TL), TLDatePicker → DatePicker/`TimePicker`/`DateTimePicker`, TLSelect → Select, TLDropdownSelect → Autocomplete (einfach/mehrfach, Nachladen der Optionen), TLOptionChips → Chip-Gruppe, TLSegmentedChoice → ToggleButtonGroup, TLChoiceGroup → RadioGroup/`FormGroup`, TLTabBar → Tabs, TLWindow/`TLDialog` → Paper/`DialogTitle`/`DialogContent`/`DialogActions`/`Backdrop`, TLMenu → MenuList/`MenuItem`, TLSnackbar → Snackbar+`Alert`, TLAlert → Alert, TLFormField → FormControl/`FormLabel`/`FormHelperText` (innerhalb der Grid-Struktur des TL-Formulars), TLText → Typography/`Chip`, TLCard → Card, TLAppBar → AppBar, TLBreadcrumb → Breadcrumbs, TLProgress → LinearProgress, TLSlider → Slider.
- Fenster und Dialoge verwenden nicht MUIs Modal: Positionierung, Verschieben/Größe, Fokusfalle, Escape und Stapelung bleiben beim TL-Fenstermanager, MUI liefert nur die Optik. Menüs werden wie TLMenu mit usePopover positioniert und schließen über useCloseOnOutsidePress (ein Klick neben das Menü erreicht das Element darunter). Popups von Auswahl und Datumsauswahl tragen anchoredOverlayProps.
- Je Adapter ein vitest-Test der Verdrahtung (Zustand → Darstellung, Benutzeraktion → Kommando), dazu Tests für installMui, die Theme-Brücke und die Farbschemata, insgesamt 251 Tests (npm test im Modul).
Theme-Brücke: MUI-Theme des Kunden → Gestaltungswerte der TL-Komponenten (aus #29662)
Ziel: Eine Anwendung, in der beliebig viele TL-Komponenten durch MUI-Komponenten ersetzt sind, sieht mit dem MUI-Theme des Kunden im Ganzen wie eine MUI-Anwendung aus. Tabelle, Baum, Panels und Layout bleiben TL-Komponenten, passen aber optisch zu den MUI-Komponenten daneben – für das Auge, nicht pixelgenau.
- Die MUI-Komponenten erhalten das MUI-Theme des Kunden unverändert.
- installMui leitet aus demselben Theme Werte für die CSS-Variablen ab, die die TL-Komponenten lesen – die Rollen des Design Systems (--tl-…) und die Tokens der UI-Themes, die Tabelle, Baum, Seitenleiste und Fenster noch lesen –, so wie MUI eigene abgeleitete Werte bildet, und schreibt sie als ein <style>-Element hinter die TL-Stylesheets (ein Farbschema: für alle Modi; Hell und Dunkel: je data-tl-mode). Kein Build-Schritt, keine erzeugte Theme-Datei, keine neuen --tl-*-Namen.
- Abgebildet werden: Palette, Schriftfamilien, Schriftgrößen und Zeilenhöhen der Textrollen, Eckenradius, Höhe der Bedienelemente, Schatten (angehoben, Popover/Menü, Ziehen, Dialog). Zuordnungstabelle im Leitfaden docs/faq/customer-component-library.md, Abschnitt "Following the MUI theme".
- Die Werte des Kunden gelten unverändert; eine Kontrastprüfung korrigiert sie nicht.
- Nicht erreichbar (literale Werte der TL-Stylesheets): Zeilenhöhe der Tabelle (rowHeight des Servers, 36px), Höhen von Tabellenkopf und -leisten, Zeilenhöhe und Schriftgröße des Baums. Die Abstandsskala wird nicht abgebildet.
Ein Farbschema hält die ganze Seite
Ein MUI-Theme mit nur einem Farbschema (z.B. nur hell) hält die ganze Seite in diesem Schema, egal welches UI-Theme gewählt ist – auch die nicht abgebildeten Gestaltungswerte (Kategoriefarben, z.B. des Avatars im Benutzermenü, und die Tokens der UI-Themes); vorher wurde die Seite halb dunkel. Dazu bietet das Theme-Skript der Seite lockMode('light' | 'dark' | null) (auch in tl-react-bridge): solange ein Modus gehalten wird, nennt data-tl-mode diesen Modus, und ein gewähltes UI-Theme des anderen Modus wird durch das wählbare UI-Theme des gehaltenen Modus vertreten. Die Auswahl des Benutzers bleibt erhalten (Benutzermenü), auf dem Server ändert sich nichts. installMui ruft es nur für ein Theme mit einem Schema auf.
Kaskadenebenen der React-Oberfläche
Regeln einer Anwendung (css-class) auf einem mit MUI dargestellten Element verloren gegen MUI, weil die MUI-Regeln zur Laufzeit ans Ende von <head> geschrieben werden (z.B. die Glaskarte in Demos → Gestaltung: weiße Schrift auf weißer Karte). Die Stile der React-Oberfläche sind jetzt nach CSS-Kaskadenebenen geordnet, unabhängig von ihrer Position in <head>: Engine < Komponentenbibliothek < Anwendung.
- ClientResources: ein Stylesheet hat ein optionales Attribut layer; ein Stylesheet mit Ebene wird als <style>@import url(…) layer(<name>);</style> ausgegeben, eines ohne als <link>. Die Reihenfolge der Ebenen ist konfigurierbar (layers, Standard tl, mui) und wird vor allen Stilen als @layer tl, mui; ausgegeben. Eine Anwendung kann eine eigene Ebene einfügen, z.B. tl, app-base, mui für ein globales Basis-Stylesheet, das MUI nicht überschreiben soll.
- Ebene tl: alle Stylesheets der Engine und der mitgelieferten Bibliotheken (Schriften, Icons, Design System, tlReactControls.css, chartjs, wysiwyg, react-flow) und die Theme-Tokens des UIThemeService. Ausnahme: tlCodeEditor.css und tlScriptEditor.css bleiben ohne Ebene, weil CodeMirror sein Basis-Theme zur Laufzeit ohne Ebene schreibt.
- Ebene mui: die Regeln von MUI (emotion) und die aus dem MUI-Theme abgeleiteten Gestaltungswerte.
- Ohne Ebene: die Stylesheets der Anwendung; sie haben Vorrang vor Engine und MUI, unabhängig von der Spezifität.
- Der GWT-Client des Flussdiagramms (TLReactFlowClient) lädt das GWT-Standard-Theme nicht mehr; es setzte Schrift und Schriftgröße von body, select und Tabellenzellen auf der ganzen Seite.
- Leitfaden: docs/faq/react-theme-tokens.md (Abschnitt "Cascade layers"), docs/faq/mui-customer-theme.md ("Where customer CSS goes").
Demo `com.top_logic.demo.react.mui` (`tl-demo-react-mui`)
Genau in der Form eines Kundenprojekts: das Theme (customerTheme.ts), seine Schriften und der Aufruf installMui({ theme: customerTheme, replace: 'all' }); das Bündel (ca. 2 kB) importiert MUI aus tl-react-mui, ein Vite-Plugin lässt den Build scheitern, sobald eine zweite MUI-Kopie entstünde. Theme ist das MUI-Beispiel-Theme "Onepirate" (MIT) mit einem selbst gestalteten dunklen Schema: hell nahezu schwarze, dunkel hellgraue Primärfarbe, pinke Sekundärfarbe, Work Sans und Roboto Condensed (Fontsource, OFL, mitgebündelt), eckige Schaltflächen in Großbuchstaben. tl-demo-react startet mit dem Maven-Profil mui (MAVEN_ARGS=-Pmui) in diesem Design; ohne Profil bleibt die TL-Oberfläche unverändert.
Das Beispielmodul com.top_logic.demo.react.corporate (Profil corporate-example) aus #29643 entfällt; der Leitfaden docs/faq/customer-component-library.md verwendet das MUI-Modul als Beispiel.
Dokumentation
- docs/faq/customer-component-library.md: Mechanismus, Material-UI-Modul, Zuordnung der Theme-Werte.
- docs/faq/mui-customer-theme.md: Anleitung für eine Anwendung im MUI-Theme eines Kunden – Theme-Modul mit installMui, Auswahl der Komponenten, Hell und Dunkel, Umgestalten über das Theme, eigene Adapter (styled) für TL-Komponenten und zusätzliche MUI-basierte Komponenten.
Lücken
Stellen, an denen Vertrag oder Bridge für eine vollständige Ersetzung nicht ausreichen:
Zustandsvertrag
- ButtonState: kein loading.
- TextInputState: kein maxLength.
- SelectState.Option und Optionen von DropdownSelectState: kein Kennzeichen für nicht wählbare Optionen.
- DatePickerState: kein Anzeigeformat; die Ersetzung formatiert nach ihrer Locale, Sekunden gehen beim Zurücksenden verloren.
- DropdownSelectState: keine serverseitige Suche; Fehler beim Nachladen der Optionen sind für eine Ersetzung nicht sichtbar (useTLCommand meldet keinen Fehler), daher kein "Erneut versuchen".
- TabBarState: keine schließbaren oder deaktivierten Reiter. MenuState: keine Untermenüs, keine ankreuzbaren Einträge außer active. WindowState: keine Hilfe-Schaltfläche.
- BreadcrumbState: kein Icon, kein Deaktiviert-Kennzeichen je Eintrag; der aktuelle Eintrag ist nur durch seine Position (der letzte) bestimmt.
- SliderState: keine Markierungen, keine senkrechte Ausrichtung. ProgressState: kein Farbton (Erfolg/Fehler), kein Pufferwert.
- TextState/`FormFieldState`: ein Rich-Tooltip ist nur über den Tooltip-Mechanismus der Bridge (data-tooltip="key:tooltip") erreichbar.
- LabelPosition.auto ist im Vertrag deklariert, wird einem Feld aber nie gesendet (das Formular löst sie auf).
Bridge
- Hilfsfunktionen der TL-Felder sind nicht exportiert (FieldValue, fieldStateAttrs, showsValueOnly); eine Ersetzung bildet Nur-Lese-Darstellung und aria-invalid/`aria-required` selbst nach.
- Die Geometrie eines Fensters (Verschieben, Größe ändern, Maximieren, gemerkte Größe) ist nicht exportiert; das MUI-Modul enthält eine Kopie der Logik von TLWindow.
- useCloseOnOutsidePress behandelt einen Klick in ein portiertes Popup einer Ersetzung (z.B. die Optionsliste eines Autocomplete in einem TL-Popover) als Außenklick; Elemente mit anchoredOverlayProps sollten als innen gelten.
- useFocusTrap springt an den Enden nicht um: Tab vom letzten Element verlässt die Seite für einen Schritt (Fokus auf body), Umschalt+Tab vom ersten Feld erreicht nicht das letzte (gilt auch für TLWindow).
- Die React-Shims (react-shim.ts, react-dom-shim.ts) muss jedes Modul selbst pflegen, das eine Bibliothek mitbündelt; vorkompilierter Bibliothekscode braucht die vollständige öffentliche API.
- Die TL-Module liefern keine TypeScript-Deklarationen aus; die paths eines Kundenmoduls auf tl-react-bridge/`tl-react-mui` funktionieren nur neben einem Quellstand der Engine.
Abweichungen ohne MUI-Gegenstück
- customOrder (Sortieren der gewählten Werte per Ziehen) und die Typeahead-Liste ohne Filterfeld (noFilter) von TLDropdownSelect; Farbrollen der Kategorien (category-N).
- Eine einfache Segmentauswahl ist in MUI eine Folge von Tab-Stopps statt einer Radio-Gruppe mit Pfeiltasten.
- Ein Switch kann den Zustand "nicht gesetzt" eines dreiwertigen Feldes nicht darstellen.
- Während das Menü eines MUI-Select offen ist, setzt MUI aria-hidden auf die übrigen Elemente der Seite.
Beobachtungen an der TL-Oberfläche (unabhängig von MUI)
- Das Überlaufmenü einer Toolbar gibt den Fokus nach Escape an das vorher fokussierte Element zurück, nicht an den Auslöser, wenn es mit der Maus geöffnet wurde (beabsichtigt, der Auslöser nimmt beim Mausklick keinen Fokus).
- Die mobile Seitenleiste ("Navigation öffnen") schließt nicht mit Escape.
- Die Fußleiste eines Fensters zeigt eine senkrechte Trennlinie zwischen der Aktionsgruppe (Abbrechen) und der Gruppe der BUTTON_BAR-Befehle (Anlegen).
- Kein Demo-View verwendet TLToggleButton außerhalb eines Menüs.
Migration
- Stylesheets einer Anwendung, die über ClientResources registriert sind, liegen in keiner Kaskadenebene und haben damit Vorrang vor allen Stilen der Engine (Ebene tl), unabhängig von der Spezifität. Eine Regel der Anwendung, die bisher gegen eine spezifischere Engine-Regel verlor, wirkt jetzt. Globale Regeln (Resets, Element-Selektoren wie button { … }), die die Engine nicht überschreiben sollen, bekommen eine eigene Ebene vor tl: die Ebene in layers eintragen und das Stylesheet mit layer="…" registrieren.
- Ein Modul, das ein Stylesheet der Engine oder einer mitgelieferten Bibliothek registriert, gibt layer="tl" an (docs/faq/new-react-module.md).
- !important kehrt sich mit Ebenen um: ein !important in einer früheren Ebene hat Vorrang vor einem in einer späteren.
- In Anwendungen mit dem Flussdiagramm-Modul ist die Schriftgröße von body jetzt die des Browsers (16px) statt 13px aus dem GWT-Standard-Theme.