major
#29596
TL Views: Gestaltungsoptionen für app-spezifische Oberflächen (css-class, Text-Varianten, Layout-Optionen, Anzeigevarianten für Eingaben, Theme-Tokens)
Kontext
Eine Gap-Analyse der Immobilien-Anwendung (estate-agent, handgeschriebenes React-Frontend mit dunklem "Glas"-Design, Serifen-Überschriften, Foto-Kachelrastern, Filter-Seitenleiste mit Chips, Segment-Schaltern und Schiebereglern) gegen die React-Oberfläche (.view.xml-Ebene) hat ergeben: Die Struktur (App-Shell, Navigation, Routing, Tabellen, Formulare, Dialoge, Diagramme, Theme-System mit Dark-Mode) ist vorhanden, aber ein app-spezifisches Erscheinungsbild lässt sich nicht ausdrücken, weil die Elemente keine Stilanker und nur ein grobes Layout-Vokabular haben.
Das Theme selbst (Farben, Schriften, Icon-Sets) bleibt app-spezifisch und wird in der Anwendung konfiguriert. Das Engine-Ticket liefert nur die Optionen und Tokens, die dafür fehlen. Grundsatz: Optionen an bestehenden Elementen statt neuer Elemente.
Erweiterung
Das Ticket wird in Phasen auf einem Branch umgesetzt (mehrere Pushes/CI-Läufe).
Phase 1: `css-class` für alle Elemente
UIElement.Config erhält die Eigenschaft css-class, die jedes Control an sein Wurzelelement schreibt. Die bestehende gleichnamige Eigenschaft von <text> wandert in die Basis-Konfiguration; <chart cssClass> wird darauf umgestellt. Damit kann eine App-Stylesheet-Regel (registriert über ClientResources) einzelne Panels, Karten, Stacks, Bilder oder Buttons gestalten ("diese eine Karte ist eine Glas-Karte").
Phase 2: Text-Varianten, Ton und Pille
<text> erhält
- variant (Typ-Rolle): display, headline, title, body (Standard), label, caption;
- tone (Farbrolle): primary (Standard), secondary, helper, accent, success, warning, error, on-color;
- appearance: text (Standard) oder pill (bestehende TLPill-Darstellung; Farbe aus tone).
Jede Ausprägung wird als stabile CSS-Klasse (tlText--display, tlText--tone-accent, …) gerendert und aus Theme-Tokens gespeist.
Neue Theme-Tokens mit neutralen Vorgabewerten, die die Controls konsumieren, damit ein App-Theme sie überschreiben kann:
- font-family-display (Schrift für display/`headline`/`title`; Vorgabe = font-family),
- border-radius-03 (dritte Radius-Stufe für große Flächen),
- surface-blur (Backdrop-Weichzeichnung für Flächen; Vorgabe 0),
- shadow-glow (Leuchtschatten für Fokus/Hervorhebung; Vorgabe = kein Schatten),
- background-image (Hintergrund der App-Shell; Vorgabe = keiner).
Transparente Flächen, Körnung, Zweitakzent u. ä. bleiben App-Stylesheet.
Phase 3: Layout-Optionen
- <stack>: wrap, justify und max-width (zentrierte Inhaltsspalte).
- <grid>: max-width. (max-columns als Obergrenze für auto-fit, z. B. 3 Karten pro Zeile, liefert #29597 über die gemeinsamen Rasteroptionen von <grid> und <object-list layout="grid">.)
Hero-Abschnitte mit Hintergrundbild und Überlagerung werden per App-CSS auf einem css-class-Stack realisiert; dafür ist kein Engine-Element nötig.
Phase 4: Anzeigevarianten für Eingabefelder
Die Field-Control-Provider erhalten eine display-Option, die per <input-control>-Annotation am Attribut bzw. am <field> gesetzt wird:
- SelectControlProvider: dropdown (Standard), chips (Mehrfachauswahl als Tag-Wolke mit umschaltbaren Pillen), segmented (Einfachauswahl als Segment-Schalter mit gleitender Markierung);
- NumberInputControlProvider: input (Standard), slider (mit min, max, step, angezeigtem Wert);
- BooleanControlProvider: checkbox (Standard), switch.
<value-input> nimmt dieselben Optionen entgegen.
Phase 5: `<value-input>` als Suchfeld
<value-input> erhält icon, placeholder, clearable und debounce (Verzögerung, bevor der Wert in den Kanal geschrieben wird), damit ein freistehendes Suchfeld mit Lupe ohne eigenes Element konfigurierbar ist (zusammen mit dem vorhandenen on-submit und <query-bindings>). (placeholder ist bereits mit #29608 geliefert.)
Phase 6: Übergangs-Anker
Dialog-Öffnen/-Schließen und Frame-Wechsel im tile-stack bekommen CSS-übergangsfähige Zustandsklassen, sodass App-CSS Ein-/Ausblendungen definieren kann. prefers-reduced-motion wird weiterhin beachtet. Die Animationen selbst bleiben App-CSS.
Lösung
Umgesetzt in zwei Pushes auf demselben Branch: Phasen 1, 2, 3 und 5, danach Phasen 4 und 6.
Phase 1
UIElement.Config#getCssClass() (css-class) ist die eine Konfigurationseigenschaft für alle Elemente. Auf Control-Seite gibt es genau einen Zustandsschlüssel cssClass an der Basisklasse ReactControl (setCssClass, in scriptingPresentationKeys() aufgenommen; Unterklassen ergänzen ihre Schlüssel per presentationKeys(super.scriptingPresentationKeys(), …)); die bisherigen gleichnamigen Einzelschlüssel von ReactTextControl, ReactHtmlControl, ReactImageControl, ReactStackControl, ReactOverlayControl, ReactResourceCellControl entfallen. Jedes der 66 Elemente reicht seine konfigurierte Klasse an das zurückgegebene Control weiter (es gibt keinen zentralen Erzeugungspunkt; Kind-Controls werden clientseitig ohne Wrapper-Element gemountet). Ein Element, das das Control eines anderen Elements durchreicht (view, tile), schreibt die Klasse nur, wenn es selbst eine konfiguriert hat. Clientseitig baut die Bridge-Funktion rootClassName(state, …) das className des Wurzelelements jeder der 78 registrierten Komponenten. <text> sendet seinen Überlauf-Modifikator als eigenen Zustandsschlüssel overflow statt in der Klassenzeichenkette; das Enum TextOverflow liegt beim Control. Die abweichend geschriebene Eigenschaft cssClass von <chart> entfällt; es gilt die geerbte css-class. FAQ: docs/faq/react-view-layer.md (Abschnitt „Styling a single element“), docs/faq/new-ui-element.md.
Phase 2
variant, tone, appearance sind drei ExternallyNamed-Enums (TextVariant, TextTone, TextAppearance in com.top_logic.layout.react.control.common) an TextElement.Config, die als getrennte Zustandsschlüssel an TLText gehen (Standardwerte werden mitgesendet) und dort zu tlText--<variant>, tlText--tone-<tone>, tlText--pill werden. Jeder Ton ist eine Regel, die die Custom Property --tlText-tone füllt; Textfarbe und Pillenfarbe lesen sie. appearance="text" behält das heutige Verhalten (ein Wert mit Modellfarbe wird als Pille dargestellt); appearance="pill" rendert immer eine Pille, deren Farbe aus tone kommt, sofern der Wert keine Modellfarbe trägt; ein Text ohne Inhalt zeichnet keine Pille. Die Umfärbung von Text in der primären App-Bar greift nur noch auf tlText--tone-primary, sodass ein explizit gesetzter Ton dort gewinnt.
Typografie: display und headline erhalten eigene Tokens (display-01-font-size/`-line-height`, heading-04-font-size/`-line-height`) und font-family-display; title = heading-03 (+ neues heading-03-line-height), body = body-compact-01, label = heading-compact-02, caption = label-01 (+ neues label-01-line-height). Töne: primary/`secondary`/`helper`/`on-color`/`error` nutzen die vorhandenen text-*-Tokens; neu als Ref-Tokens text-accent → interactive, text-success → support-success, text-warning → support-warning.
Weitere Tokens im default-Theme (Dark erbt): font-family-display (Ref auf font-family), border-radius-03 (1rem, dritte Radius-Stufe „große Fläche“; docs/faq/react-theme-tokens.md beschreibt drei Stufen), surface-blur (Länge, 0`; vom Dialog-Backdrop konsumiert), `shadow-glow (Text, none), background-image (Text, none; von body in tlReactBase.css konsumiert).
Phase 3
<stack>: wrap (boolean), justify (Enum ReactStackControl.StackJustify: start, center, end, space-between, space-around, space-evenly, Regeln .tlStack--justify-*) und max-width. max-width liegt an der gemeinsamen Basis ReactLayoutControl (Zustandsschlüssel maxWidth, Klasse tlBounded: width: 100%, margin-inline: auto) und für Raster auf den gemeinsamen GridOptions, gilt also für <grid> und <object-list> (Raster- und Listenanordnung). Typ ist wie bei <panel width> eine CSS-Länge als Zeichenkette. Die Anordnungsschlüssel des Stacks sind in der Headless-Projektion als reine Darstellung deklariert. FAQ-Abschnitt „Layout: stack and grid“.
Phase 4
Die Anzeigevarianten sind Optionen der vorhandenen Field-Control-Provider (com.top_logic.layout.view.form), gesetzt über den bestehenden <input-control>-Mechanismus: als TLInputControl-Annotation am Attribut oder Typ, als input-control eines <field> und - neu - als input-control eines <value-input> (ValueInputElement.Config; der typbasierte Pfad von FieldControlService.createFieldControl nimmt dafür eine Provider-Konfiguration als Vorrang-Parameter). Jede Variante ist ein reiner Darstellungs-Zustandsschlüssel (display, in scriptingPresentationKeys() deklariert), sodass die Headless-Projektion in jeder Form dieselben Optionen und denselben Wert liest.
- Schalter: BooleanPresentation (Modell-Annotation <boolean-display presentation="switch"/>) erhält die Ausprägung switch; die klassische Oberfläche behandelt sie wie checkbox (CheckboxInputTag, PrimitiveColumn, AttributeOperations). BooleanControlProvider.Config#display (@NullDefault) überschreibt die Annotation für ein Feld. ReactCheckboxControl sendet display=switch; TLCheckbox rendert <input type=checkbox role=switch class="tlReactCheckbox--switch"> mit Schiene und Knopf aus Theme-Tokens. Ein Tristate-Wert bleibt Kontrollkästchen (ein Schalter hat keine dritte Stellung).
- Chips / Segmente: SelectControlProvider.Config#display mit Enum SelectDisplay (dropdown, chips, segmented; com.top_logic.layout.react.control.select). Ein Server-Control ReactDropdownSelectControl hält Optionsindex und Werteprotokoll für alle Formen und wählt das React-Modul (TLDropdownSelect, TLOptionChips, TLSegmentedChoice); eine Form, die alle Optionen zeigt, bekommt die Optionsliste sofort und bei jeder Wertänderung (invalidateOptions()), das Dropdown lädt weiterhin erst beim Öffnen. Client: selectOptions.tsx (gemeinsamer Optionsdeskriptor, Pille, Bild, Nur-Lese-Wert), TLOptionChips (Toggle-Buttons aria-pressed; Einfachauswahl schaltet um, Mehrfachauswahl toggelt), TLSegmentedChoice (radiogroup/`radio` mit Pfeiltasten, gleitender Marker per ResizeObserver; bei Mehrfachauswahl gefüllte Segmente ohne Marker).
- Schieberegler: NumberInputControlProvider.Config mit display (Enum NumberDisplay: input, slider), min, max, step. Die Gültigkeit erzwingen Konfigurations-Constraints, sodass ein Konfigurationsformular keine ungültige Konfiguration zulässt: min/`max` sind Pflicht, sobald display="slider" ist (neue allgemeine Annotation @MandatoryIf(other=@Ref(DISPLAY), value="slider") in com.top_logic.basic.config.constraint.annotation, vergleicht die Konfigurations-Schreibweise des anderen Werts), min < max (@ComparisonDependency), step > 0 (@Bound). Eigenes Control ReactSliderControl (Modul TLSlider): der Wert wird als Zahl ausgetauscht (nicht als Text im Locale-Format) und über NumberFormats.normalize in den Zahltyp des Felds gebracht; die Beschriftung neben dem Griff (valueLabel) schreibt der Server im Format des Felds. Client <input type=range> + <output>, Senden entprellt und beim Loslassen.
Tests: TestBooleanControlProvider, TestSelectControlProvider, TestNumberInputControlProvider (inkl. der Constraint-Fälle), TestMandatoryIf, TestValueInputElement. FAQ: docs/faq/react-view-layer.md, Abschnitt „Display variants of input fields“.
Phase 5
icon (kodiertes ThemeImage, z. B. css:fa-solid fa-magnifying-glass), clearable und debounce werden wie placeholder (#29608) Eigenschaften der FieldSpec, in ReactFieldControlProvider.createField auf jedes Feld-Control angewendet, als Zustandsschlüssel icon, clearable, debounceMs von ReactFormFieldControl an den Client geschickt (reine Darstellung; placeholder bleibt in der Projektion) und in TLTextInput umgesetzt: führendes Icon, Löschen-Schaltfläche (erscheint nur bei Inhalt, schreibt den leeren Wert sofort, Fokus zurück ins Feld), konfigurierbare Verzögerung. Die Konstante VALUE_DEBOUNCE_MS (300 ms) ist einmal in der Bridge definiert; Text-, Zahl- und Passwortfeld lesen debounceMs. Ein Feld, das erst beim Verlassen sendet (URL-Feld, Zahl), ignoriert die Verzögerung. <value-input> deklariert icon, clearable, debounce (@Format(MillisFormat.class), z. B. debounce="300ms").
Phase 6
Ein Bridge-Hook useKeyedTransition({keys, node, container, enterClass, exitClass}) (react-src/bridge/transition.ts, exportiert aus tl-react-bridge) markiert die Änderung dessen, was ein Control anzeigt: ein neu erscheinender Schlüssel bekommt an seinem Element die Eintritts-Klasse, ein verschwindender Schlüssel wird durch eine inerte Kopie seines zuletzt angezeigten Elements (aria-hidden, inert, ohne ids, pointer-events: none) mit der Austritts-Klasse im Container ersetzt. Beides endet mit animationend/`transitionend` am Element selbst, mit dem Wiedererscheinen des Schlüssels oder nach TRANSITION_FALLBACK_MS (1000 ms, einmal definiert). Die erste Anzeige und - unabhängig vom App-CSS - ein Viewer mit prefers-reduced-motion: reduce erhalten weder Klasse noch Kopie.
Konsumenten und Klassen:
- Wizard: tlWizard__step--entering / tlWizard__step--exiting, tlWizard--forward|backward; behält seine Standardanimation. Der bisherige Inline-Code kopierte den Schritt nach dem DOM-Tausch und erwischte damit den ankommenden statt des verlassenen Schritts (im Browser bestätigt); der Hook merkt sich das Element jedes Schlüssels nach jedem Commit und kopiert das tatsächlich verlassene.
- Dialog: tlDialog__backdrop--entering am Backdrop des geöffneten Dialogs, tlDialog__backdrop--exiting an der Kopie des geschlossenen (der TLDialogManager ist der Container und bleibt daher auch ohne Dialoge gerendert). Die Kopie deckt die Seite und lässt alle Eingaben durch.
- Tile-Stack: tlTileStack__frame--entering / tlTileStack__frame--exiting und tlTileStack--forward|backward (aus der Änderung des activeIndex abgeleitet).
Die Engine liefert für Dialog und Tile-Stack nur die strukturellen Regeln der Kopie (position: absolute; inset: 0; pointer-events: none bzw. pointer-events: none), keine Animation. FAQ: Abschnitt „Transitions“ in docs/faq/react-view-layer.md.
Demo
com.top_logic.demo.react: Seite „Gestaltung“ (demo/styling-demo.view.xml) mit allen Text-Varianten und Tönen, Pillen aus Ton und aus Modellfarbe, Hero-Band und Glas-Karte über css-class aus tl-demo-react.css, Stack-/Raster-Optionen, Suchfeld sowie dem Abschnitt „Darstellungsvarianten einer Eingabe“ (Formular über ein transientes demo.react:Demo mit Chips, Segmenten, Schiebereglern und Schalter, dazu <value-input>`s mit `input-control). tl-demo-react.css animiert die Übergangs-Klassen (Dialog blendet ein/aus, Tile-Stack-Frames gleiten je nach Richtung). Zusätzliches Demo-Theme glass (erweitert default), das surface-blur, shadow-glow und font-family-display setzt.
Nicht Teil dieses Tickets: generischer Repeater (object-list), Bild-Erweiterung, Wizard/Stepper, Anzeige lang laufender Aufträge, Anzeige von HTML-Dokumenten/Druck. Dafür gibt es eigene Tickets.