15 KiB
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(mitvariantfuer Scroll) und seinen Slots. - Jeder Inhaltsblock im Body = eine Region (ein
Panelmitvariant= Regionstyp). toolbarexistiert 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 SlotStackLayout.Toolbarist optional fuer seitenweite Toolbars.
Architektur-Prinzipien
- URL ist Source-of-Truth fuer Navigation (aktiver Tab, aktive View, entityId). Kein
useStatefuer Tabs. - Jeder Seiteninhalt ist eine Region (Panel mit Variant). Generische Aenderungen an einem Regionstyp wirken systemweit.
- Keine Fallbacks: Fehlender Kontext = Error, nicht stiller Default.
- Kein RBAC im UI: Sichtbarkeit via Backend-Flags.
- 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.
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
<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:
/* Panel.module.css */
.panel[data-variant="table"] .body {
min-height: 300px; /* wirkt systemweit fuer alle Tabellen */
}
Beispiel: Collapsible Dashboard
<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
interface LayoutTabItem {
id: string;
label: string;
icon?: ReactNode;
group?: string; // Gruppierung (Kategorie-Titel)
disabled?: boolean;
render: () => ReactNode;
}
Beispiel: Gruppierte Tabs
<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
selectedMandateIdenthalten - Metriken/Dashboard konsistent mit Tabelle: Gleicher Scope
2. FK-Label-Resolution
- displayField-Konvention:
{fieldName}Label(z.B.workflowId->workflowIdLabel) enrichRowsWithFkLabelsin ALLEN Pfaden (Standard + filterValues)- Cross-Mandate FK-Lookup:
getRecord()statt mandate-scoped Methoden
3. FormGeneratorTable Integration
hookData.refetchals 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 gegenuserMandateIds
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.tsxregistriert 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-keybeibehalten — 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 |