# Layout-System -- Referenz ## Uebersicht Das Layout-System definiert, wie jede Seite in ui-nyla strukturiert wird. Es ersetzt das manuelle Pattern (`adminPage`/`adminPageFill`/`pageHeader`/`tableContainer` aus `Admin.module.css`) und alle Ad-hoc-Tab-Implementierungen durch ein einheitliches Komponentensystem. Quell-Plan: `pagelayout_component_system_fd9fde3a.plan.md` (v3) | Komponente | Datei | Zweck | |------------|-------|-------| | `StackLayout` | `components/Layout/StackLayout.tsx` | Seiten-Container mit Slots (Header, Toolbar, Tabs, Body, Footer) | | `Panel` | `components/Layout/Panel.tsx` | Typisierte Region (Card, Table, Dashboard, Toolbar, Editor, Wizard) mit Collapse/Expand | | `LayoutTabs` | `components/Layout/LayoutTabs.tsx` | Einziges Tab-System, URL als Source-of-Truth | | `ViewStack` | `components/Layout/ViewStack.tsx` | Master-Detail-Navigation (list/catalog/detail via URL) | | `useScrollMode` | `hooks/useScrollMode.ts` | Scroll-Modus-Erkennung (bounded/document) | ## Begriffe (WICHTIG, nicht verwechseln) Drei verschiedene Konzepte, die sauber getrennt werden muessen: | Begriff | Anzahl | Werte | Bedeutung | |---------|--------|-------|-----------| | **Slot** | 5 | Header, Toolbar, Tabs, Body, Footer | Benannte Bereiche des `StackLayout`-Seitenrahmens (Compound-Components, z.B. ``) | | **StackLayout-Variant** | 4 | table, scroll, form, dashboard | Scroll-Verhalten der gesamten Seite (Prop `variant` an `StackLayout`) | | **Region** | beliebig | — | Ein `Panel`-Inhaltsblock im Body. Das ist der collapsible/expandable Block, den der User sieht. | | **Regionstyp** | 6 | card, table, dashboard, toolbar, editor, wizard | Die Variante einer Region (Prop `variant` an `Panel`) | Merksatz: - Eine **Seite** = ein `StackLayout` (mit `variant` fuer Scroll) und seinen **Slots**. - Jeder **Inhaltsblock** im Body = eine **Region** (ein `Panel` mit `variant` = Regionstyp). - `toolbar` existiert sowohl als Slot (`StackLayout.Toolbar`) als auch als Regionstyp (`Panel variant="toolbar"`). In der Praxis werden Filter-/Action-Bars als **Region** (`Panel variant="toolbar"`) im Body platziert; der Slot `StackLayout.Toolbar` ist optional fuer seitenweite Toolbars. ## Architektur-Prinzipien 1. **URL ist Source-of-Truth** fuer Navigation (aktiver Tab, aktive View, entityId). Kein `useState` fuer Tabs. 2. **Jeder Seiteninhalt ist eine Region** (Panel mit Variant). Generische Aenderungen an einem Regionstyp wirken systemweit. 3. **Keine Fallbacks**: Fehlender Kontext = Error, nicht stiller Default. 4. **Kein RBAC im UI**: Sichtbarkeit via Backend-Flags. 5. **Persistence ist pluggable**: Panel-Collapse in localStorage, Navigation in URL, Daten in DB. ## Scroll-Vertrag ### Problem Die Layout-Kette ist durchgehend `overflow:hidden` mit `flex-shrink:0` fuer Chrome-Bereiche. Auf kleinen Viewports fuellt der Header die Seite, die Tabelle wird unbedienbar. ### Loesung: scrollMode-Zustandsmaschine | Modus | Bedingung | Verhalten | |-------|-----------|-----------| | `bounded` | Viewport > 1024px Breite UND > 500px Hoehe | `overflow:hidden`-Kette intakt. Tabelle scrollt intern. Header bleibt sichtbar. | | `document` | Viewport <= 1024px ODER <= 500px Hoehe | `overflow:visible`-Kette. Seite scrollt als Dokument. Header scrollt weg. Tabelle waechst natuerlich. | ### Mechanismus Hook `useScrollMode()` setzt `document.documentElement.dataset.scrollMode`. CSS-Module nutzen `:global(html[data-scroll-mode="document"])` als Selektor-Prefix. ```tsx import { useScrollMode } from '../../hooks/useScrollMode'; const MyPage = () => { const scrollMode = useScrollMode(); // scrollMode wird automatisch auf StackLayout propagiert return ...; }; ``` ### Layout-Kette (verifiziert) ``` MainLayout.mainLayout height:100dvh; overflow:hidden MainLayout.content overflow:hidden (desktop) / auto (mobile) MainLayout.outletShell overflow-y:auto FeatureLayout.featureContent overflow:hidden; flex:1 FeatureView.viewContent overflow:auto; padding:1.5rem StackLayout root data-scroll-mode + data-variant Panel data-variant (card|table|dashboard|toolbar|editor|wizard) FormGeneratorTable height:100%; flex:1; overflow:hidden ``` ## StackLayout Container fuer eine gesamte Seite oder einen Tab-Inhalt. Compound-Component-Pattern mit benannten **Slots**. ### Slots | Slot | Zweck | Beispiel | |------|-------|---------| | `Header` | Seitentitel, Breadcrumb | Immer sichtbar | | `Toolbar` | Seitenweite Filter, Actions | Kontextbezogen (optional) | | `Tabs` | LayoutTabs-Container | Tab-Navigation | | `Body` | Hauptinhalt — enthaelt die Regionen (Panels) | Tabelle, Formular, Dashboard | | `Footer` | Status, Aktionen | Optional | ### StackLayout-Varianten (Scroll-Verhalten) | Variant | Body-Verhalten | |---------|---------------| | `table` | flex:1, min-height:0, bounded scroll | | `scroll` | Normaler Scroll (Standard) | | `form` | Scroll mit Padding | | `dashboard` | Grid-freundlich, natuerliche Hoehe | ### Beispiel: Tabellen-Seite ```tsx

Benutzer

``` ## Panel (Region) Typisierter Container fuer jeden Inhaltsblock. Jede Panel-Instanz hat einen `variant`, der das Layout-Verhalten bestimmt. ### Props | Prop | Typ | Default | Beschreibung | |------|-----|---------|-------------| | `variant` | `card \| table \| dashboard \| toolbar \| editor \| wizard` | `card` | Bestimmt CSS-Verhalten (Flex, Padding, Border) | | `title` | `string \| ReactNode` | - | Header-Titel (wenn gesetzt, wird Header gerendert) | | `subtitle` | `string \| ReactNode` | - | Untertitel im Header | | `actions` | `ReactNode` | - | Action-Buttons im Header | | `collapsible` | `boolean` | `false` | Collapse/Expand-Toggle im Header | | `defaultCollapsed` | `boolean` | `false` | Initial-Zustand | | `collapseKey` | `string` | - | localStorage-Key fuer Persistenz (`panel-collapse:{key}`) | ### Varianten-Verhalten | Variant | flex | Padding | Border | Typischer Inhalt | |---------|------|---------|--------|-----------------| | `card` | - | 14px | Standard-Card | Info-Sektionen, Settings, Formulare | | `table` | flex:1, min-height:0 | 0 | Standard-Card | FormGeneratorTable | | `dashboard` | - | 14px | Standard-Card | KPI-Grid, Stats-Cards | | `toolbar` | - | 8px 14px | kein Radius | Filter-Bar, Actions, kompakt | | `editor` | flex:1, min-height:0 | 0 | Standard-Card | Code-Editor, Flow-Editor, Chat | | `wizard` | - | 20px | Standard-Card | Wizard-Schritte | ### Generische Aenderungen CSS-Aenderungen an `[data-variant="table"]` in `Panel.module.css` wirken auf ALLE Tabellen-Regionen im System. Beispiel: ```css /* Panel.module.css */ .panel[data-variant="table"] .body { min-height: 300px; /* wirkt systemweit fuer alle Tabellen */ } ``` ### Beispiel: Collapsible Dashboard ```tsx
``` ## LayoutTabs Einziges Tab-System. Ersetzt `UiComponents/Tabs` und alle manuellen Tab-Button-Implementierungen. ### Props | Prop | Typ | Default | Beschreibung | |------|-----|---------|-------------| | `items` | `LayoutTabItem[]` | - | Tab-Definitionen | | `urlParam` | `string` | - (kein Default) | URL-Search-Parameter fuer aktiven Tab | | `defaultTab` | `string` | erstes Item | Fallback wenn URL-Param fehlt | | `preserveSearchParams` | `boolean` | `true` | Andere URL-Params beim Tab-Wechsel erhalten | | `aliasMap` | `Record` | - | URL-Wert -> Tab-ID Mapping | | `syncUrl` | `boolean` | `!!urlParam` | Tab-Wechsel schreibt in URL (default: an, wenn `urlParam` gesetzt) | | `lazy` | `boolean` | `false` | Wenn `true`: besuchte Tabs bleiben gemountet (`display:none`), State bleibt erhalten. Wenn `false`: nur aktiver Tab gemountet, Wechsel remountet (State-Verlust). Fuer Tab-Sets mit Formularen/Streams `lazy` setzen. | | `collapsible` | `boolean` | `false` | Tab-Bar einklappbar | ### LayoutTabItem ```typescript interface LayoutTabItem { id: string; label: string; icon?: ReactNode; group?: string; // Gruppierung (Kategorie-Titel) disabled?: boolean; render: () => ReactNode; } ``` ### Beispiel: Gruppierte Tabs ```tsx }, { id: 'runs', label: 'Durchlaeufe', group: 'Ausfuehren', render: () => }, { id: 'editor', label: 'Editor', group: 'Erstellen', render: () => }, { id: 'templates', label: 'Vorlagen', group: 'Erstellen', render: () => }, ]} /> ``` ## ViewStack Master-Detail-Navigation. Modi `list | catalog | detail` via URL-Parameter. ### Props | Prop | Typ | Default | Beschreibung | |------|-----|---------|-------------| | `viewParam` | `string` | `'view'` | URL-Parameter fuer aktive View | | `entityParam` | `string` | - | URL-Parameter fuer Entity-ID (z.B. `runId`) | | `defaultView` | `ViewMode` | `'list'` | Fallback-View | ### View-Modi | Modus | URL-Beispiel | Verhalten | |-------|-------------|-----------| | `list` | `/workflows` | Listenansicht (FormGeneratorTable) | | `catalog` | `/workflows?view=catalog` | Katalog/Grid-Ansicht | | `detail` | `/workflows?view=detail&runId=abc` | Detail-Ansicht mit Entity | ## Seiten-Aufbau-Muster ### Muster 1: Einfache Tabellen-Seite ``` StackLayout (variant="table") └── Body ├── Panel (variant="toolbar"): Header-Actions, Filter └── Panel (variant="table", collapsible): FormGeneratorTable ``` Beispiele: AdminUsersPage, AdminMandatesPage, PromptsPage, ConnectionsPage ### Muster 2: Dashboard-Seite ``` StackLayout (variant="dashboard") └── Body ├── Panel (variant="dashboard"): KPI-Grid ├── Panel (variant="card", collapsible): Themen/Module └── Panel (variant="card", collapsible): Details/Info ``` Beispiele: TrusteeDashboardView, CommcoachDashboardView, TeamsbotDashboardView ### Muster 3: Seite mit Tabs ``` StackLayout (variant="table") └── Body ├── Panel (variant="toolbar"): Kontext-Filter (optional) └── LayoutTabs (urlParam="tab") ├── Tab "uebersicht": Panel (variant="dashboard") + Panel (variant="table") └── Tab "settings": Panel (variant="card") ``` Beispiele: WorkflowAutomationHubPage, Settings, ComplianceAuditPage ### Muster 4: Wizard-Seite ``` StackLayout (variant="form") └── Body ├── Panel (variant="toolbar"): Step-Indicator + Navigation └── Panel (variant="wizard"): Step-Inhalt ``` Beispiele: AdminMandateWizardPage, AdminInvitationWizardPage ### Muster 5: Editor/Chat-Seite ``` StackLayout (variant="scroll") └── Body └── Panel (variant="editor"): Editor/Chat (flex:1, volle Hoehe) ``` Beispiele: WorkflowEditorPage, ChatStream ### Muster 6: Split-Layout (Phase 6b PanelLayout) ``` PanelLayout (config-getriebener Split-Tree) ├── Panel links: Sidebar (collapsible, resizable) ├── Panel mitte: Hauptinhalt └── Panel rechts: Detail/Preview (collapsible, resizable) ``` Beispiele: WorkspacePage, FilesPage, CommcoachDossierView, RedmineBrowserView ## Migrations-Checkliste Bei JEDER Seiten-Migration abarbeiten (Lessons aus Phase 4): ### 1. Daten & API - Mandantenfilter: "Alle" = kein `mandateId`-Param, Backend filtert per User-Mandatsliste - Daten-Reload bei Kontextwechsel: Dependencies muessen `selectedMandateId` enthalten - Metriken/Dashboard konsistent mit Tabelle: Gleicher Scope ### 2. FK-Label-Resolution - displayField-Konvention: `{fieldName}Label` (z.B. `workflowId` -> `workflowIdLabel`) - `enrichRowsWithFkLabels` in ALLEN Pfaden (Standard + filterValues) - Cross-Mandate FK-Lookup: `getRecord()` statt mandate-scoped Methoden ### 3. FormGeneratorTable Integration - `hookData.refetch` als einziger Datenpfad - Keine redundanten Filter-Buttons (FormGeneratorTable hat eingebaute Filter) - Kein RBAC im UI ### 4. Backend RBAC - Kein `isPlatformAdmin`-Bypass fuer Lesezugriffe - Fail-closed: Fehlender Filter = leeres Ergebnis (`__impossible__`-Sentinel) - `mandateId`-Validierung gegen `userMandateIds` ### 5. Layout & UI - StackLayout + Panel verwenden, URL als Source-of-Truth - Panel fuer collapsible Bloecke - scrollMode-Integration: min-height im document-Mode ### 6. Keep-Alive (nur fuer registrierte Seiten) - Pruefen ob Seite in `config/keepAliveRoutes.tsx` registriert ist - Bei Wiedereinblenden Re-Measure/Re-Clamp ausloesen (Hoehe war `display:none` -> 0) - `matchLocation`-Bedingung bei Tab-basierten Keep-Alive-Routes erhalten (z.B. `?tab=editor`) - `scopeKey` (Mandant/Instanz) als React-`key` beibehalten — Scope-Wechsel = Neu-Mount - Resize-/SSE-Listener-Cleanup darf persistenten State nicht zerstoeren ## Anti-Patterns | Anti-Pattern | Stattdessen | |-------------|-------------| | `styles.adminPage` / `adminPageFill` | `StackLayout` mit passender Variante | | `styles.tableContainer` | `Panel variant="table"` | | `styles.pageHeader` mit inline Actions | `Panel variant="toolbar"` oder `StackLayout.Header` | | Manuelle Tab-Buttons (`onClick`, `activeTab` State) | `LayoutTabs` mit `urlParam` | | `UiComponents/Tabs` | `LayoutTabs` | | `useState` fuer aktiven Tab | URL via `urlParam` | | `isPlatformAdmin` Bypass im Backend | Mandate-scoped Filter mit fail-closed | | `modalOverlay` CSS per Seite dupliziert | (siehe separater Modal-Konsolidierungsplan) | ## Dateien | Datei | Zweck | |-------|-------| | `components/Layout/StackLayout.tsx` + `.module.css` | Seiten-Container | | `components/Layout/Panel.tsx` + `.module.css` | Typisierte Region | | `components/Layout/LayoutTabs.tsx` + `.module.css` | Tab-System | | `components/Layout/ViewStack.tsx` + `.module.css` | Master-Detail | | `components/Layout/types.ts` | Shared TypeScript-Typen | | `hooks/useScrollMode.ts` | Scroll-Modus-Erkennung | | `config/keepAliveRoutes.tsx` | Keep-Alive-Registrierung (persistente Seiten) | | `layouts/MainLayout.tsx` + `.module.css` | App-Shell (Sidebar + Content) | | `layouts/FeatureLayout.tsx` + `.module.css` | Feature-Chrome (Breadcrumb, Kontext) | | `pages/FeatureView.tsx` + `.module.css` | View-Router fuer Feature-Instanzen |