# 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) | | `PanelLayout` | `components/Layout/PanelLayout.tsx` | Config-getriebene Split-Panes (resize, collapse, localStorage) | | `FloatingPortal` | `components/UiComponents/FloatingPortal/FloatingPortal.tsx` | Floating Dropdowns/Popovers auf `document.body` (Viewport-Positionierung, Click-Outside) | | `useScrollMode` | `hooks/useScrollMode.ts` | Scroll-Modus-Erkennung (bounded/document) | | `useDocumentTitle` | `hooks/useDocumentTitle.ts` | Seitentitel (`${appName} - ${titel}`), route-gated fuer Keep-Alive | | `useScrollRestoration` | `hooks/useScrollRestoration.ts` | Scroll-Position pro Route (integriert in StackLayout) | | `useVisibilityRemeasure` | `hooks/useVisibilityRemeasure.ts` | Re-Measure nach `display:none` (Keep-Alive, FormGeneratorTable) | | `tableFilterPersistence` | `utils/tableFilterPersistence.ts` | Filter/Suche in localStorage mit Scope-Key (L10) | ## 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. 6. **Floating UI via Portal**: Dropdowns, Popovers und Autocomplete-Menues rendern ueber `FloatingPortal` auf `document.body` — nie `position:absolute` in overflow-geclippten Ahnen. Kein per-Seite-zIndex-Workaround. ## Floating UI (FloatingPortal) ### Problem `overflow:hidden`/`overflow:clip` in Layout-Panes clippt klassische `position:absolute`-Dropdowns. Z-Index-Erhoehungen pro Seite loesen das nicht zuverlaessig. ### Loesung `FloatingPortal` rendert Kinder per React-Portal auf `document.body`, positioniert relativ zum Anchor (`getBoundingClientRect`), mit Viewport-Clamping und Click-Outside-Schliessen. | Prop | Default | Zweck | |------|---------|-------| | `open` | — | Sichtbarkeit | | `anchorRef` | — | Referenz-Element (Button/Input) | | `onClose` | — | Click-Outside / Schliessen | | `placement` | `auto` | `top` / `bottom` / `auto` (Platz pruefen) | | `align` | `start` | `start` / `center` / `end` | | `keepMounted` | `false` | Kinder beim Schliessen gemountet lassen (z.B. Chat-Picker prefetch) | ### Migrierte Konsumenten (Stand 2026-06-10) `DropdownSelect`, `ProviderSelector`, `UserSection`, `NotificationBell`, `PeriodPicker`, `TableViewsBar`, `RagRunningBadge`, `AddressAutocomplete`, `CanvasHeader`, `WorkspaceInput`, `FormGeneratorTable`-Filter, Workspace Chat-Picker. ### Anti-Pattern | Falsch | Richtig | |--------|---------| | `.dropdownMenu { position:absolute; z-index:9999 }` in Panel/Table | `FloatingPortal` + Anchor-Ref | | `WorkspaceInput zIndex:2` um Menues sichtbar zu machen | Overflow-Kette fixen + Portal | | `position:fixed` mit hardcodierten `left`/`bottom` (NotificationBell alt) | `FloatingPortal` + Anchor | ## Overflow-Kette (2026-06-10) | Ebene | Regel | |-------|-------| | `PanelLayout` Root | `overflow:hidden` nur am aeusseren Split-Container | | `PanelLayout` `.pane` / `.paneBody` | kein `overflow:hidden` (clippt Floating-UI-Vorfahren nicht) | | `Panel variant="editor"` | `overflow:visible` (Chat/Editor mit Dropdowns) | | Scroll-Container | explizit markierte `.body`-Regionen mit `overflow:auto` | | UDB / FilesTab | `flex:1; min-height:0` fuer volle Sidebar-Hoehe | ### 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 (PanelLayout) ``` PanelLayout (persistenceKey, direction="horizontal", panes=[...]) ├── Pane links: Sidebar (collapsible, resizable) — oft Panel variant="card" ├── Pane mitte: Hauptinhalt — oft Panel variant="editor" oder "table" └── Pane rechts: Detail/Preview (collapsible, resizable) ``` Verschachtelte Split-Baeume: `PanelLayout` in Pane-`content` erneut einsetzen. Collapse-Toggle: Chevron-Icons (`FaChevronLeft/Right/Up/Down`) am inneren Pane-Rand, nicht Text (`»`/`«`). Persistenz via `collapseKey` → `panel-collapse:{key}` in localStorage. ```tsx }, { id: 'table', defaultSize: 72, content: ... }, ]} /> ``` Beispiele: FilesPage, CommcoachSessionView, RedmineBrowserView ### Muster 7: AI Workspace (Prompt-Centric + Explorer-Kontext) Kein `PanelLayout`-3-Spalten-Split mehr fuer Kontext. Stattdessen: ``` StackLayout (variant="table") └── Body (layoutFill) └── workspaceShell ├── topBar: Kontext-Toggle (Icon) · Chatname (`...` wenn neu) · Chat-Picker · Neuer-Chat (+) └── mainStage (flex row, volle Hoehe) ├── [optional] WorkspaceContextSidebar (320px, volle Hoehe) │ ├── contextToolbar: Icons Dateien | Quellen | Aktivitaet | Vorschau | Collapse │ └── contextSidebarBody: FilesTab | SourcesTab | ToolActivityLog | FilePreview └── centerColumn: ChatStream + WorkspaceInput ``` URL-Parameter: `?ctxTab=files|sources|activity|preview` (Legacy `data` → `files`). Kontext-Panel offen: `localStorage workspace-ctx-open-{instanceId}`. **Desktop:** Kontext oeffnen setzt Sidebar immer auf expandiert (`ctxSidebarCollapsed=false`). Collapse nur ueber Chevron in der Sidebar-Toolbar. **Mobile (<=1024px):** Prompt-centric — Top-Bar (Chat-Picker + Kontext-Icon), Chat fuellt Stage. Kontext als Bottom-Sheet (66vh, per Grab/↑ auf Vollbild). Im Sheet: `allowCollapse={false}`. Komponente: `pages/views/workspace/WorkspaceContextSidebar.tsx`. Chats leben im FloatingPortal-Picker (nicht in der Kontext-Sidebar). Beispiel: `WorkspacePage` (Keep-Alive) ## 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` | | Absolute Dropdown-Menues in Panels | `FloatingPortal` | | Per-Seite zIndex fuer Dropdowns | Overflow-Kette + Portal | | `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 | | `components/UiComponents/FloatingPortal/FloatingPortal.tsx` + `.module.css` | Portal-basierte Floating UI | | `pages/views/workspace/WorkspaceContextSidebar.tsx` | AI-Workspace Kontext-Sidebar (Explorer-Style) | | `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 |