wiki/b-reference/ui-nyla/layout.md
2026-06-11 15:44:54 +02:00

463 lines
20 KiB
Markdown

<!-- status: canonical -->
<!-- lastReviewed: 2026-06-10 -->
<!-- verifiedAgainst: ui-nyla Layout-Primitives (StackLayout, Panel, PanelLayout, LayoutTabs, ViewStack, FloatingPortal, WorkspaceContextSidebar, useScrollMode, useDocumentTitle, tableFilterPersistence) -->
# 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.Body>`) |
| **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 <StackLayout variant="table">...</StackLayout>;
};
```
### 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
<StackLayout variant="table">
<StackLayout.Header>
<h2>Benutzer</h2>
</StackLayout.Header>
<StackLayout.Body>
<Panel variant="toolbar">
<FilterBar />
</Panel>
<Panel variant="table" title="Benutzerliste" collapsible>
<FormGeneratorTable ... />
</Panel>
</StackLayout.Body>
</StackLayout>
```
## 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
<Panel variant="dashboard" title="Uebersicht" collapsible collapseKey="dashboard-overview">
<div className={styles.kpiGrid}>
<StatCard label="Workflows" value={7} />
<StatCard label="Aktiv" value={5} />
</div>
</Panel>
```
## 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<string, string>` | - | 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
<LayoutTabs
urlParam="tab"
defaultTab="workflows"
items={[
{ id: 'workflows', label: 'Workflows', group: 'Ausfuehren', render: () => <WorkflowsTab /> },
{ id: 'runs', label: 'Durchlaeufe', group: 'Ausfuehren', render: () => <RunsTab /> },
{ id: 'editor', label: 'Editor', group: 'Erstellen', render: () => <EditorTab /> },
{ id: 'templates', label: 'Vorlagen', group: 'Erstellen', render: () => <TemplatesTab /> },
]}
/>
```
## 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
<PanelLayout
persistenceKey="files-main"
panes={[
{ id: 'tree', defaultSize: 28, collapsible: true, collapseKey: 'files-tree', content: <FormGeneratorTree ... /> },
{ id: 'table', defaultSize: 72, content: <Panel variant="table">...</Panel> },
]}
/>
```
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 |