372 lines
15 KiB
Markdown
372 lines
15 KiB
Markdown
<!-- status: canonical -->
|
|
<!-- lastReviewed: 2026-06-10 -->
|
|
<!-- verifiedAgainst: ui-nyla Layout-Primitives (StackLayout, Panel, LayoutTabs, ViewStack, useScrollMode) -->
|
|
|
|
# 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.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.
|
|
|
|
## 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 <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 (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 |
|