# VotianLT – UI Theme & Gestaltungsrichtlinien > Gilt für alle Views unter `backend/src/main/java/de/assecutor/votianlt/pages/view/` > Theme-Datei: `backend/src/main/frontend/themes/votian-modern/styles.css` > Stand: UI-Änderungen bis 23.03.2026 berücksichtigt (`Landing-Hero-CTA/Demo-Button`, `ViewToolbar`-Migrationen, Landing-/Dashboard-Updates, TabSheet-Dialoge, Message-/Statistics-Layouts) --- ## 1. Design-Prinzipien - **Konsistenz**: Alle Views nutzen dieselben Klassen, Abstände und Farben. - **Kein Overflow**: Kein View darf horizontal scrollen. Alle Container haben `box-sizing: border-box`, `max-width: 100%` und `min-width: 0`. - **Kein externes Margin/Padding am View-Root**: Views füllen ihren Container vollständig aus. Abstände zum Bildschirmrand entstehen ausschließlich durch den `view-container`. - **Lumo-Overrides**: Vaadin-Lumo-Standardstile werden durch gezielte CSS-Regeln mit höherer Spezifizität überschrieben – **nicht** mit inline Styles in Java-Code lösen. --- ## 2. Farben & Custom Properties Alle Farben sind als CSS-Custom-Properties auf `html` definiert und **müssen** statt hartcodierter Hex-Werte verwendet werden. ### Hauptfarben | Variable | Wert | Verwendung | |---|---|---| | `--app-accent` | `#2563eb` | Primäraktionen, Links, Highlights | | `--app-accent-strong` | `#1d4ed8` | Hover-Zustand von Primärbuttons | | `--app-accent-soft` | `rgba(37,99,235,0.12)` | Selektierter Zeilenhintergrund | | `--app-success` | `#059669` | Erfolgsstatus, abgeschlossene Aufgaben | | `--app-warning` | `#d97706` | Warnungen | | `--app-danger` | `#dc2626` | Fehler, kritische Zustände | ### Oberflächen & Hintergründe | Variable | Wert | Verwendung | |---|---|---| | `--app-surface` | `rgba(255,255,255,0.78)` | Karten, Panels (Glasmorphismus) | | `--app-surface-solid` | `#ffffff` | Volldeckende Flächen | | `--app-surface-muted` | `rgba(247,250,255,0.88)` | Gedämpfte Panels | | `--app-shell-background` | Radial+Linear-Gradient | Seitenhintergrund (Body) | | `--app-sidebar-background` | Linear-Gradient dunkelblau | Drawer/Sidebar | ### Text | Variable | Wert | Verwendung | |---|---|---| | `--app-text-strong` | `#0f172a` | Überschriften, wichtige Labels | | `--app-text` | `#1e293b` | Fließtext | | `--app-text-muted` | `#64748b` | Sekundärtext, Beschreibungen | ### Ränder | Variable | Wert | Verwendung | |---|---|---| | `--app-border` | `rgba(148,163,184,0.18)` | Subtile Trennlinien | | `--app-border-strong` | `rgba(148,163,184,0.28)` | Sichtbare Kartenränder | | `--app-field-border` | `#d6dde7` | Formularfeld-Rand | | `--app-field-border-hover` | `#c6d0dd` | Formularfeld-Rand (Hover) | ### Schatten Alle globalen Schatten-Variablen sind auf `none` gesetzt – das Design ist bewusst schattenlos. | Variable | Wert | Verwendung | |---|---|---| | `--app-shadow-sm` | `none` | – | | `--app-shadow-md` | `none` | – | | `--app-shadow-lg` | `none` | – | | `--app-shadow-xl` | `none` | – | Auch die Lumo-Schatten-Overrides sind `none`: ```css --lumo-box-shadow-xs: none; --lumo-box-shadow-s: var(--app-shadow-sm); /* → none */ --lumo-box-shadow-m: var(--app-shadow-md); /* → none */ --lumo-box-shadow-l: var(--app-shadow-lg); /* → none */ ``` **Ausnahme**: Buttons behalten eigene Schatten (siehe Abschnitt 24). --- ## 3. Typografie **Schriftart**: `Manrope` (Google Fonts, Gewichte 400–800) **Fallbacks**: `Avenir Next`, `Segoe UI`, `sans-serif` Lumo-Überschreibungen: ```css --lumo-font-family: "Manrope", "Avenir Next", "Segoe UI", sans-serif; --lumo-base-color: #f5f7fb; ``` Alle `h1`–`h6` haben `letter-spacing: -0.03em` und `color: var(--app-text-strong)`. ### Responsive Schriftgrößen (`clamp`) | Element | Wert | |---|---| | `.form-title` | `clamp(1.55rem, 3vw, 2.2rem)`, font-weight 800 | | `.section-title` | `clamp(1.4rem, 3.4vw, 2rem)`, font-weight 800 | | `.hero-panel-title` | `clamp(2rem, 5vw, 3.4rem)`, font-weight 800 | | `.dashboard-title` | `clamp(1.7rem, 3.2vw, 2.5rem)`, font-weight 800 | | `.login-card-title` | `clamp(1.8rem, 4vw, 2.4rem)`, font-weight 800 | **Regel**: Überschriften mit `font-weight: 800` und `letter-spacing: -0.05em` bis `-0.06em` für Großtitel. --- ## 4. Layout-Struktur (App Shell) ``` vaadin-app-layout (336px Drawer) ├── [part=drawer] ← Sidebar (Dunkelblau-Gradient) │ ├── .app-drawer-header │ ├── .app-drawer-scroll (Scroller, flex: 1) │ │ └── .app-nav-container │ │ └── .app-nav-tree (TreeGrid) │ └── .app-user-menu └── [Light DOM default slot] └── .view-container ← Routed View Container └── ``` ### `view-container` Der Container, in dem alle gerouteten Views dargestellt werden: ```css .view-container { display: flex; flex-direction: column; background: transparent; margin: 20px; /* Abstand zum Browserrand */ width: calc(100% - 40px); height: calc(100dvh - 40px); overflow: hidden; border-radius: 12px; box-sizing: border-box; } ``` **Wichtig**: Views, die in `.view-container` landen, erhalten automatisch: ```css flex: 1 1 0; min-height: 0; min-width: 0; max-width: 100%; box-sizing: border-box; ``` --- ## 5. View-Klassen Jede View muss **genau eine** der folgenden Root-Klassen tragen: | Klasse | Verwendung | Extends | |---|---|---| | `data-view` | Listen, Tabellen, Grids | `Main` oder `VerticalLayout` | | `form-page` | Formulare mit zentriertem Inhalt | `VerticalLayout` | | `admin-form-view` | Admin-Formulare | `VerticalLayout` | | `message-hub-view` | Nachrichten-Übersicht | `Main` | | `statistics-chat-view` | Statistik/Chat | `VerticalLayout` | | `dashboard-view` | Dashboard-Seiten | `VerticalLayout` | | `dashboard-home-view` | Haupt-Dashboard (transparenter Hintergrund) | `VerticalLayout` | | `landing-view` | Startseite (Shell-Gradient im Body, weiße Inhalts-Panels) | `Main` | | `login-view` | Login-Seite (Shell-Gradient, einspaltig, fullscreen) | `Main` | ### Kritische Java-Regeln ```java // ✅ Richtig für Daten-Views: addClassName("data-view"); // KEIN setJustifyContentMode(CENTER) — wird per CSS auf START erzwungen // ✅ Richtig für Formular-Views (zentrierter Inhalt): addClassName("form-page"); setJustifyContentMode(JustifyContentMode.CENTER); setDefaultHorizontalComponentAlignment(Alignment.CENTER); // ❌ Falsch: setPadding(true) auf View-Root oder form-shell ohne form-card // → wird per CSS auf 0 zurückgesetzt, erzeugt keine visuelle Wirkung ``` --- ## 6. Interne Layout-Klassen ### `form-shell` – Vollbreite-Wrapper ```java VerticalLayout layout = new VerticalLayout(); layout.setPadding(false); layout.setSpacing(true); layout.setWidthFull(); layout.addClassName("form-shell"); ``` **Regeln**: - Kein `setPadding(true)` → hat keine Wirkung, da via CSS überschrieben - Kein horizontales Margin - `box-sizing: border-box`, `max-width: 100%`, `min-width: 0` ### `form-card` – Schmales zentriertes Karten-Formular ```java container.addClassNames("form-shell", "narrow", "form-card"); container.setWidth("400px"); container.setMaxWidth("100%"); ``` Bekommt **weißen Hintergrund** (0.9 Alpha), `border-radius: 28px`, `backdrop-filter: blur(18px)`. ### `surface-panel` – Datenpanel / Tabellenkarte ```java Div panel = new Div(grid); panel.addClassNames("surface-panel", "data-grid-panel"); panel.setWidthFull(); ``` ```css .surface-panel { border: 1px solid var(--app-border-strong); background: var(--app-surface); backdrop-filter: blur(18px); box-shadow: var(--app-shadow-lg); border-radius: 28px; box-sizing: border-box; max-width: 100%; } .data-grid-panel { padding: 0.8rem; min-height: 420px; box-sizing: border-box; max-width: 100%; } ``` --- ## 7. ViewToolbar Einheitliche Toolbar für alle Seiten-Überschriften: ```java add(new ViewToolbar(getTranslation("my.view.title"))); // Mit Aktionsbutton: add(new ViewToolbar(getTranslation("my.view.title"), myButton)); // Mit gruppierten Sekundäraktionen: add(new ViewToolbar( getTranslation("my.view.title"), ViewToolbar.group(backButton, exportButton), addButton)); ``` ```css .view-toolbar { width: 100%; padding: 0.75rem 1rem; box-sizing: border-box; } .view-toolbar-title-row { gap: 0.8rem; } .view-toolbar-title { color: var(--app-text-strong); font-weight: 800; letter-spacing: -0.05em; } .view-toolbar-actions, .view-toolbar-group { width: 100%; justify-content: flex-end; } ``` **Regeln**: - `ViewToolbar` ist Standard für neue Header und ersetzt ad-hoc `HorizontalLayout`+`H2`/`H1`-Konstrukte. - Bereits migriert: `EditProfileView`, `ImprintView`, `StatisticsView`, `UserMessagesView`, `MessageDetailsView`. - Titel im Toolbar-Header bleiben einzeilig (`white-space: nowrap` wird im Component-Code gesetzt). - Ausnahme: Für komplexe Header-Zeilen wie in `MessageDetailsView` darf der Inhalt einer `ViewToolbar` ersetzt werden, die Basiskomponente bleibt aber `ViewToolbar`. --- ## 8. Daten-Grids Standard-Muster für alle Tabellen-Views: ```java // View-Root setSizeFull(); addClassName("data-view"); // ViewToolbar add(new ViewToolbar(getTranslation("title"), addButton)); // Grid Grid grid = new Grid<>(T.class, false); grid.setWidthFull(); grid.setHeightFull(); grid.addClassName("data-grid"); // Panel-Wrapper Div panel = new Div(grid); panel.addClassNames("surface-panel", "data-grid-panel"); panel.setWidthFull(); add(panel); ``` **Grid-Styling**: - Hintergrund: `rgba(255,255,255,0.88)`, `border-radius: 24px` - Header: Gedämpftes Grau, Uppercase, `font-weight: 800`, `font-size: 0.76rem` - Selektierte Zeile: `--app-accent-soft` Hintergrund - Auch eingebettete Grids in Formular-/Summary-Bereichen werden so gekapselt; kein nacktes `Grid` direkt in einen Content-Abschnitt hängen. --- ## 9. Formulare (Form-Views) ```java // View-Root setSizeFull(); setJustifyContentMode(JustifyContentMode.CENTER); setDefaultHorizontalComponentAlignment(Alignment.CENTER); setPadding(true); addClassName("form-page"); // Formular-Container VerticalLayout container = new VerticalLayout(); container.setWidth("600px"); container.setMaxWidth("100%"); container.setPadding(true); container.setSpacing(true); container.addClassNames("form-shell", "form-card"); // Formular-Felder FormLayout form = new FormLayout(); form.setWidthFull(); form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 2)); ``` **Formularfelder** haben automatisch: - `border-radius: 20px` - `background: #ffffff` - `border: 1px solid var(--app-field-border)` - Hover: Blaue Highlight-Farbe - Fokus: Blauer Ring `rgba(37,99,235,0.18)` --- ## 10. Filter-Panels ```java HorizontalLayout filterBar = new HorizontalLayout(); filterBar.addClassName("filter-panel"); filterBar.setWidthFull(); ``` ```css .filter-panel { padding: 1rem 1.15rem; border-radius: 26px; flex-wrap: wrap; gap: 1rem; min-width: 0; max-width: 100%; box-sizing: border-box; } ``` **Wichtig**: Filter-Felder **niemals** mit fixen Pixel-Breiten versehen → `flex-wrap: wrap` und `min-width: 0` sorgen für korrektes Responsive-Verhalten. --- ## 11. Dashboard Home View Die `dashboard-home-view` hat einen transparenten Hintergrund, der den Shell-Gradient durchscheinen lässt. ```css .dashboard-home-view { background: transparent !important; } .view-container:has(.dashboard-home-view) { border-radius: 12px; background: transparent; overflow: auto; } body:has(.dashboard-home-view) { background: var(--app-shell-background); } /* Surface-Panels und Hero-Panels im Dashboard: kein Border, kein Schatten */ .dashboard-home-view .surface-panel { border: 0; background: transparent; backdrop-filter: none; box-shadow: none; } .dashboard-home-view .hero-panel { box-shadow: none; } ``` **Aktuelles Strukturmuster**: - Das eingeloggte Dashboard besteht aus Hero + Systemsektion; App-Overview und Footer bleiben auf der öffentlichen Landing Page. - Dashboard-Feature-Karten dürfen vollständig klickbar sein, indem `feature-card` in `RouterLink.feature-card-link` gewrappt wird. ```java RouterLink link = new RouterLink(); link.setRoute(ShowJobsView.class); link.add(card); link.addClassName("feature-card-link"); ``` --- ## 12. Sidebar / Navigation ### Drawer Header (klickbar) Der Drawer-Header (Logo + App-Name) ist klickbar und navigiert zum Dashboard: ```java header.getStyle().set("cursor", "pointer"); header.getElement().setProperty("title", "Zum Dashboard"); header.addClickListener(event -> UI.getCurrent().navigate("dashboard")); // AdminLayout navigiert zu "admin-dashboard" ``` ### Nav-Rows ```css .app-nav-row { border-radius: 18px; border: 1px solid rgba(255,255,255,0.06); background: rgba(255,255,255,0.08); max-width: calc(100% - 4px); transition: transform 0.18s ease, background-color 0.18s ease; } .app-nav-row:hover { transform: translateX(4px); background: rgba(255,255,255,0.14); } .app-nav-row-root { width: 20.5rem; } .app-nav-row-management-child { width: 18.25rem; } .app-nav-row-user-child { width: 17rem; } ``` **Zusatzregeln**: - Root-Einträge und Kind-Einträge bekommen unterschiedliche Breitenklassen entsprechend ihrer Ebene im `TreeGrid`. - Drawer-Labels werden im Renderer mit `white-space: nowrap` versehen; Zeilenumbrüche in der Navigation sind nicht vorgesehen. ### User Menu ```css .app-user-menu { margin: auto 0.5rem 0.5rem; padding: 0.45rem 0.65rem; border-radius: 22px; border: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.08); backdrop-filter: blur(14px); } ``` --- ## 13. Karten-Hierarchie | Klasse | Radius | Hintergrund | Einsatz | |---|---|---|---| | `surface-panel` | 28px | rgba(255,255,255,0.78) + Blur | Hauptdatenpanels | | `form-card` | 28px | rgba(255,255,255,0.90) + Blur | Formular-Cards | | `section-card`, `content-card` | 26px | rgba(255,255,255,0.88) | Abschnittspanels | | `simple-card` | 26px | rgba(255,255,255,0.88) | Einfache Karten | | `detail-card` | 24px | rgba(255,255,255,0.88) | Detail-Informationen | | `hero-panel` | 34px | Dunkelblau-Gradient | Hero-Bereiche | | `feature-card` | 24px | Linear-Gradient weiß/blau | Feature-Kacheln, optional klickbar via `feature-card-link` | | `message-card` | 22px | Linear-Gradient weiß | Nachrichten | | `station-tile` | 24px | Linear-Gradient weiß | Station-Kacheln | | `job-task-card` | 22px | Linear-Gradient weiß | Aufgaben-Karten | --- ## 14. Station Tiles & Kacheln ### Station Tile ```css .station-tile { border-radius: 24px; border: 1px solid var(--app-border-strong); background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(244,248,255,0.9)); box-shadow: var(--app-shadow-sm); transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; } .station-tile:hover { transform: translateY(-3px); box-shadow: var(--app-shadow-md); border-color: rgba(37,99,235,0.24); } .station-tile.validated { background: linear-gradient(180deg, rgba(236,253,245,0.98), rgba(209,250,229,0.88)); border-color: rgba(5,150,105,0.26); } ``` ### Add Station Tile (gestrichelt) ```css .add-station-tile { background: linear-gradient(180deg, rgba(241,245,249,0.9), rgba(248,250,252,0.98)); border-style: dashed; } ``` ### Stations Grid ```css .stations-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; width: 100%; } ``` --- ## 15. Job Task Cards ```css .job-task-card { display: flex; align-items: center; gap: 1rem; padding: 1rem; border: 1px solid var(--app-border-strong); border-radius: 22px; background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(246,249,255,0.9)); box-shadow: var(--app-shadow-sm); transition: transform 0.2s ease, box-shadow 0.2s ease; cursor: pointer; } .job-task-card:hover { transform: translateY(-2px); box-shadow: var(--app-shadow-md); border-color: rgba(37,99,235,0.24); } .job-task-card.completed { border-color: rgba(5,150,105,0.22); background: linear-gradient(180deg, rgba(236,253,245,0.98), rgba(220,252,231,0.88)); } ``` ### Task Status Badges ```css .job-task-status.open { background: rgba(220,38,38,0.12); color: var(--lumo-error-text-color); } .job-task-status.completed { background: rgba(5,150,105,0.12); color: var(--lumo-success-text-color); } ``` --- ## 16. Dashboard Stat Cards ```css .dashboard-stat-card { position: relative; border-radius: 24px; padding: 1.15rem; border: 1px solid var(--app-border-strong); background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(246,249,255,0.92)); box-shadow: var(--app-shadow-md); } .dashboard-stat-card::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 4px; background: var(--dashboard-accent, var(--app-accent)); } ``` ### Akzent-Farben ```css .accent-blue { --dashboard-accent: #2563eb; } .accent-green { --dashboard-accent: #059669; } .accent-purple { --dashboard-accent: #7c3aed; } .accent-orange { --dashboard-accent: #d97706; } .accent-gray { --dashboard-accent: #64748b; } .accent-red { --dashboard-accent: #dc2626; } ``` --- ## 17. Message & Chat Components ### Header & Thread Layout Message-Views verwenden `ViewToolbar` als erstes sichtbares Header-Element. Eigene `.message-thread-header`-Layouts sollen für neue Implementierungen nicht mehr aufgebaut werden. ```java addClassName("message-hub-view"); VerticalLayout contentLayout = new VerticalLayout(); contentLayout.setPadding(true); contentLayout.setSpacing(true); contentLayout.setWidthFull(); contentLayout.setHeightFull(); contentLayout.addClassName("message-thread-layout"); contentLayout.add(new ViewToolbar(getTranslation("usermessages.title.with", clientName))); ``` ```css .message-thread-layout { width: 100%; margin: 0; padding: 0; background: transparent; border: none; box-shadow: none; } .message-thread-scroller { border: 1px solid var(--app-border-strong); background: linear-gradient(180deg, rgba(249,250,251,0.95), rgba(243,244,246,0.88)); border-radius: 24px; overflow-y: auto !important; } .message-thread { padding: 1rem !important; } .message-thread-input { border: 1px solid var(--app-border-strong); background: rgba(255,255,255,0.82); border-radius: 22px; padding: 0.85rem 1rem; } ``` ### Message Card ```css .message-card { padding: 1rem; border: 1px solid var(--app-border-strong); border-radius: 22px; background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(246,249,255,0.9)); box-shadow: var(--app-shadow-sm); transition: transform 0.22s ease, box-shadow 0.22s ease; } .message-card:hover { transform: translateY(-4px); box-shadow: 0 24px 48px rgba(15,23,42,0.14); } ``` ### Chat Bubbles ```css .message-bubble { max-width: min(78%, 720px); padding: 0.85rem 1rem; border-radius: 22px; box-shadow: var(--app-shadow-sm); } .message-bubble.client { background: linear-gradient(135deg, rgba(255,255,255,0.98), rgba(241,245,249,0.92)); border: 1px solid rgba(148,163,184,0.25); } .message-bubble.server { background: linear-gradient(135deg, rgba(191,219,254,0.8), rgba(167,243,208,0.74)); border: 1px solid rgba(59,130,246,0.22); } ``` ### Chat AI/User Bubbles ```css .chat-bubble--user { background: linear-gradient(135deg, #2563eb, #1d4ed8); color: #eff6ff; padding: 0.85rem 1rem; border-radius: 24px; } .chat-bubble--ai { background: rgba(255,255,255,0.94); border: 1px solid var(--app-border-strong); padding: 1rem; border-radius: 24px; } ``` --- ## 18. Dialoge ```css vaadin-dialog-overlay::part(content), vaadin-confirm-dialog-overlay::part(content) { border-radius: 28px; border: 1px solid var(--app-border-strong); background: rgba(255,255,255,0.94); backdrop-filter: blur(20px); box-shadow: var(--app-shadow-xl); } ``` ### Dialog Panels | Klasse | Verwendung | |---|---| | `.dialog-form-panel` | Formulare im Dialog | | `.dialog-content-panel` | Inhalte im Dialog | | `.dialog-task-card` | Aufgaben-Karten im Dialog | | `.dialog-cargo-card` | Fracht-Karten im Dialog | ```css .dialog-form-panel, .dialog-content-panel, .dialog-cargo-card { backdrop-filter: blur(12px); padding: 1rem; border-radius: 24px; } .dialog-task-card { transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; } ``` ### Station Dialoge mit TabSheet und innerer Karte `PickupStationDialog` und `DeliveryStationDialog` verwenden nicht mehr die Standard-Overlay-Karte, sondern ein transparentes Overlay mit eigener weißer Innenkarte. ```java getElement().setAttribute("theme", "no-inner-card"); TabSheet tabSheet = new TabSheet(); tabSheet.setWidthFull(); tabSheet.setSizeFull(); Div frame = new Div(); frame.getStyle().set("border", "10px solid transparent"); frame.getStyle().set("display", "flex"); frame.getStyle().set("flex-direction", "column"); frame.getStyle().set("flex", "1"); frame.setSizeFull(); Div whiteCard = new Div(); whiteCard.getStyle().set("background", "white"); whiteCard.getStyle().set("border-radius", "24px"); whiteCard.getStyle().set("flex", "1"); whiteCard.getStyle().set("overflow", "auto"); whiteCard.setSizeFull(); whiteCard.add(tabSheet); frame.add(whiteCard); add(frame); ``` ```css vaadin-dialog-overlay[theme~="no-inner-card"]::part(content) { border-radius: 0; border: none; background: none; backdrop-filter: none; box-shadow: none; padding: 0; margin: 0; } vaadin-tabsheet::part(content) { border-radius: 0 0 24px 24px; background: rgba(255,255,255,0.86); } [part="tabs-container"] { background: white; border-radius: 24px 24px 0 0; border-bottom: 1px solid #e0e0e0; } ``` **Tab-Konventionen**: - `PickupStationDialog`: Adresse, Termine, Fracht - `DeliveryStationDialog`: Adresse, Aufgaben - Fehlerindikatoren sitzen direkt am jeweiligen `Tab` --- ## 19. Detail Cards ```css .detail-card { border: 1px solid var(--app-border-strong); border-radius: 24px; background: rgba(255,255,255,0.9); box-shadow: var(--app-shadow-sm); padding: 1rem; } .detail-card--accent { border-color: rgba(37,99,235,0.22); background: linear-gradient(180deg, rgba(219,234,254,0.62), rgba(239,246,255,0.9)); } .detail-card--code, .detail-card--comment { font-family: ui-monospace, "SFMono-Regular", monospace; } ``` --- ## 20. Route & Summary Cards ```css .route-card, .summary-card { border: 1px solid var(--app-border-strong); border-radius: 24px; background: rgba(255,255,255,0.84); box-shadow: var(--app-shadow-sm); padding: 1rem; } .services-panel, .notes-panel { border: 1px solid var(--app-border-strong); border-radius: 24px; background: rgba(255,255,255,0.84); box-shadow: var(--app-shadow-sm); padding: 0.75rem 1rem 1rem; } ``` --- ## 21. Login Seite Die Login-Seite verwendet ein **einspaltiges**, zentriertes Layout ohne Highlight-Panel. ```css .login-view { padding-inline: 0; background: var(--app-shell-background) !important; min-height: 100vh; min-height: 100dvh; } .login-shell { width: min(750px, 100%); display: grid; grid-template-columns: 1fr; gap: 1.5rem; } .login-card { width: 100%; padding: clamp(1.35rem, 3vw, 2rem); border-radius: 32px; border: 1px solid var(--app-border-strong); background: #ffffff; box-shadow: var(--app-shadow-lg); } .login-card vaadin-login-form-wrapper { background: #ffffff !important; } ``` ### Java-Aufbau (LoginView) ```java // Login-Card enthält direkt: flashBox, loginForm, 2FA-Felder, registerButton, versionSpan // KEIN login-highlight Panel, KEIN H1-Titel (login-card-title entfernt) loginLayout.add(flashBox, loginForm, twoFaField, verify2faButton, registerButton, versionSpan); Div loginShell = new Div(loginLayout); loginShell.addClassName("login-shell"); ``` ### Eyebrow Chip (Landing Page) ```css .eyebrow-chip { display: inline-flex; align-items: center; padding: 0.45rem 0.8rem; border-radius: 999px; background: rgba(255,255,255,0.14); color: rgba(248,250,252,0.92); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } ``` --- ## 22. Landing Page ```css .landing-view { padding: 20px; gap: 20px; background: transparent !important; min-height: 100vh; min-height: 100dvh; } body:has(.landing-view) { background: var(--app-shell-background); } .landing-view .surface-panel { border: 1px solid var(--app-border-strong); background: transparent; backdrop-filter: none; box-shadow: none; } .landing-view .landing-header, .landing-view .section-panel, .landing-view .app-overview-panel, .landing-view .footer-panel { background: var(--app-surface-solid); } ``` ```css .landing-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; } .landing-logo { font-size: clamp(1.6rem, 4vw, 2.2rem); font-weight: 800; letter-spacing: -0.05em; } .landing-nav-button, .landing-language-button { border-radius: 999px; } ``` ### Anonymer Header - Im anonymen Zustand enthält die Header-Navigation nur `Login`, `Registrieren` und die Sprachwahl. - Öffentliche Primäraktionen gehören nicht in die Header-Leiste, sondern in das Hero-Panel. - Der `Demo`-Einstieg wird deshalb nicht als zusätzlicher Header-Button geführt. ```java private Component createAnonymousNavigation() { HorizontalLayout navButtons = new HorizontalLayout(); navButtons.setSpacing(true); navButtons.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); navButtons.addClassName("landing-nav"); Button loginBtn = new Button(getTranslation("start.button.login"), event -> login()); loginBtn.addThemeVariants(ButtonVariant.LUMO_CONTRAST); loginBtn.addClassName("landing-nav-button"); Button registerBtn = new Button(getTranslation("start.button.register"), event -> register()); registerBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); registerBtn.addClassName("landing-nav-button"); Button languageBtn = createLanguageSelector(); navButtons.add(loginBtn, registerBtn, languageBtn); return navButtons; } ``` ### App-Section vs. Footer CTA-Text und Slogan liegen auf der Landing Page jetzt im `app-overview-panel`, nicht mehr im Footer. ```css .app-cta { margin-top: 0.75rem; color: var(--app-accent-strong); font-weight: 700; max-width: 820px; } .app-slogan { color: var(--app-accent-strong); font-style: italic; } ``` ### Hero Panel ```css .hero-panel { position: relative; overflow: hidden; min-height: 520px; justify-content: center; text-align: center; border-radius: 34px; border: 1px solid rgba(255,255,255,0.14); background: linear-gradient(135deg, #081224 0%, #1d4ed8 54%, #0f766e 100%); box-shadow: var(--app-shadow-xl); } .hero-panel-title { color: #f8fafc; font-size: clamp(2rem, 5vw, 3.4rem); font-weight: 800; letter-spacing: -0.06em; } .hero-panel-text { color: rgba(226,232,240,0.92); font-size: clamp(1rem, 2vw, 1.15rem); } .hero-actions { gap: 1rem; align-items: center; } .hero-action-group { gap: 0.45rem; align-items: center; } .hero-choice-hint { max-width: 420px; margin: 0; color: rgba(226,232,240,0.82); font-size: 0.95rem; line-height: 1.55; text-align: center; } ``` ### Hero-CTA-Regeln - Öffentliche CTAs werden in der Hero-Kachel vertikal gestapelt und nicht über Header und Hero verteilt. - Reihenfolge auf der Landing Page: zuerst `Demo`, danach `Jetzt kostenlos testen`. - Beide CTA-Buttons verwenden dieselbe Primärdarstellung: `LUMO_PRIMARY`, `LUMO_LARGE`, `setWidthFull()`, Pill-Optik. - Jeder CTA bekommt direkt darunter einen eigenen Erklärungstext. - Keine kombinierten Erklärungstexte für mehrere Buttons verwenden; Hinweise immer 1:1 an den jeweiligen CTA binden. ```java Button demoButton = new Button(getTranslation("start.button.demo"), event -> loginDemo()); demoButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE); demoButton.addClassNames("hero-cta", "hero-demo-cta"); demoButton.setWidthFull(); Button ctaButton = new Button(getTranslation("cta.freetest"), event -> register()); ctaButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE); ctaButton.addClassName("hero-cta"); ctaButton.setWidthFull(); Paragraph demoHint = new Paragraph(getTranslation("start.hero.demo.hint")); demoHint.addClassName("hero-choice-hint"); Paragraph trialHint = new Paragraph(getTranslation("start.hero.trial.hint")); trialHint.addClassName("hero-choice-hint"); VerticalLayout demoAction = new VerticalLayout(demoButton, demoHint); demoAction.addClassName("hero-action-group"); VerticalLayout trialAction = new VerticalLayout(ctaButton, trialHint); trialAction.addClassName("hero-action-group"); VerticalLayout heroActions = new VerticalLayout(); heroActions.setPadding(false); heroActions.setSpacing(false); heroActions.setWidthFull(); heroActions.setMaxWidth("420px"); heroActions.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); heroActions.addClassName("hero-actions"); heroActions.add(demoAction, trialAction); ``` ```properties start.hero.demo.hint=Demo startet sofort mit vorbereiteten Beispieldaten. start.hero.trial.hint="Jetzt kostenlos testen" erstellt Ihren eigenen Account für den kostenlosen Probemonat. ``` --- ## 23. Timeline Components ```css .timeline-entry-card { margin-bottom: 0.75rem; padding: 1rem; border: 1px solid var(--app-border-strong); border-radius: 24px; background: rgba(255,255,255,0.9); box-shadow: var(--app-shadow-sm); border-left: 4px solid var(--timeline-accent, var(--app-accent)); } .timeline-reason { font-weight: 700; color: var(--app-text-strong); } .timeline-timestamp { color: var(--app-text-muted); font-size: 0.78rem; } ``` --- ## 24. Invoice Components ### Invoice Generator ```css .invoice-generator-panel { backdrop-filter: blur(16px); border-radius: 24px; padding: 1rem; border: 1px solid var(--app-border-strong); background: rgba(255,255,255,0.9); } .invoice-generator-template { display: flex; align-items: center; gap: 0.75rem; padding: 0.95rem 1rem; border: 1px solid var(--app-border-strong); border-radius: 18px; background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(246,249,255,0.92)); cursor: grab; transition: transform 0.2s ease, box-shadow 0.2s ease; } .invoice-generator-template:hover { transform: translateY(-2px); box-shadow: var(--app-shadow-sm); } .invoice-generator-canvas { position: relative; overflow: hidden; border: 2px dashed rgba(148,163,184,0.5); border-radius: 24px; background: linear-gradient(180deg, rgba(255,255,255,0.95), rgba(241,245,249,0.9)), linear-gradient(90deg, rgba(148,163,184,0.08) 1px, transparent 1px), linear-gradient(rgba(148,163,184,0.08) 1px, transparent 1px); background-size: auto, 28px 28px, 28px 28px; } ``` ### Price Table ```css .price-table { width: 100%; } .price-row { display: flex; justify-content: space-between; gap: 1rem; padding: 0.35rem 0; } .price-row--strong .price-row-label, .price-row--strong .price-row-value { font-weight: 800; color: var(--app-text-strong); } ``` --- ## 25. Buttons ```css vaadin-button { font-weight: 700; border-radius: var(--lumo-border-radius-m); transition: transform 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease; } vaadin-button:not([disabled]):hover { transform: translateY(-1px); } /* Primary Buttons – eigene Schatten (Ausnahme vom globalen none) */ vaadin-button[theme~="primary"] { box-shadow: 0 8px 20px rgba(37,99,235,0.22); } vaadin-button[theme~="primary"]:not([disabled]):hover { box-shadow: 0 12px 28px rgba(37,99,235,0.32); } /* Success Primary */ vaadin-button[theme~="primary"][theme~="success"] { box-shadow: 0 8px 20px rgba(5,150,105,0.24); } vaadin-button[theme~="primary"][theme~="success"]:not([disabled]):hover { box-shadow: 0 12px 28px rgba(5,150,105,0.34); } /* Error Primary */ vaadin-button[theme~="primary"][theme~="error"] { box-shadow: 0 8px 20px rgba(220,38,38,0.24); } /* Tertiary Error Hover */ vaadin-button[theme~="tertiary"][theme~="error"]:not([disabled]):hover { background: rgba(220,38,38,0.08); transform: translateY(-1px); } /* Disabled */ vaadin-button[disabled] { opacity: 0.46; transform: none !important; } ``` ### Button-Varianten-Konvention (Java) | Rolle | Theme-Variante | Beispiel | |---|---|---| | Hauptaktion (Erstellen, Speichern, Anwenden) | `LUMO_PRIMARY` | `addButton`, `applyFilter` | | Sekundäraktion (Abbrechen, Zurück, Export, Paginierung) | `LUMO_TERTIARY` | `cancelButton`, `backButton`, `exportButton` | | Gefährliche Aktion (Löschen) | `LUMO_ERROR` | `deleteButton` | ```java // ✅ Sekundäre Buttons immer mit LUMO_TERTIARY: cancelButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); exportButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); // ✅ Primäre Buttons mit LUMO_PRIMARY: addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); ``` **Pill-Buttons** (Nav, Landing): `border-radius: 999px` --- ## 26. Overflow & Box-Model Regeln ### Pflicht bei allen Container-Elementen ```java component.getStyle() .set("box-sizing", "border-box") .set("max-width", "100%") .set("min-width", "0"); ``` ### Warum `box-sizing: border-box` kritisch ist Ohne `box-sizing: border-box` gilt: ``` Gesamtbreite = width + padding-left + padding-right + border-left + border-right ``` - `surface-panel` hat `border: 1px` → ohne box-sizing ist es `100% + 2px` → Overflow - `data-grid-panel` hat `padding: 0.8rem` → ohne box-sizing ist es `100% + 1.6rem` → Overflow ### Warum `min-width: 0` kritisch ist In Flex-Containern verhindert die Default-Eigenschaft `min-width: auto`, dass Flex-Items kleiner werden als ihr Inhalt. `min-width: 0` erlaubt das Schrumpfen unter die Inhaltsgröße. --- ## 27. Lumo-Override-Strategie ### Padding-Override ```css /* Lumo: vaadin-vertical-layout[theme~="padding"] → Spezifizität 0,0,1,1 */ /* Override braucht mindestens 0,0,2,0: */ vaadin-vertical-layout.form-shell:not(.form-card) { padding: 0; } ``` ### Inline-Style-Override `setJustifyContentMode()` setzt Inline-Styles → nur `!important` überschreibt: ```css vaadin-vertical-layout.data-view { justify-content: flex-start !important; align-items: stretch !important; } ``` **Regel**: `!important` nur für Inline-Style-Overrides verwenden. --- ## 28. Responsive Verhalten ### Breakpoints | Breakpoint | Änderungen | |---|---| | `max-width: 720px` | `view-container` Margin reduziert auf 10px, border-radius auf 8px | | `max-height: 820px` | Drawer-Header und Nav-Rows bekommen kompakteres Padding | > **Hinweis**: Login-Shell ist jetzt immer einspaltig – der alte 980px-Breakpoint entfällt. ### Mobile View-Container ```css @media (max-width: 720px) { .view-container { margin: 10px; width: calc(100% - 20px); height: calc(100dvh - 20px); border-radius: 8px; } } ``` ### Flex-Wrap-Pflicht Alle horizontalen Layouts, die Felder oder Buttons enthalten, **müssen** `flex-wrap: wrap` haben: ```java layout.getStyle().set("flex-wrap", "wrap"); ``` --- ## 29. Photo Gallery ```css .job-photo-gallery { max-width: 600px; min-height: 500px; height: 500px; position: relative; display: flex; align-items: center; justify-content: center; padding: 1rem; background: rgba(255,255,255,0.96); border-radius: 24px; } .job-photo-counter { position: absolute; top: var(--lumo-space-s); right: var(--lumo-space-s); padding: var(--lumo-space-xs) var(--lumo-space-s); border-radius: 999px; background: rgba(15,23,42,0.72); color: #f8fafc; font-weight: 700; } .job-photo-nav { position: absolute; top: 50%; transform: translateY(-50%); border-radius: 999px; background: rgba(255,255,255,0.9); box-shadow: var(--app-shadow-sm); } ``` --- ## 30. Empty State ```css .empty-state-card { text-align: center; color: var(--app-text-muted); padding: 1rem; border: 1px dashed var(--app-border-strong); border-radius: 24px; background: rgba(248,250,252,0.74); } ``` --- ## 31. Inline Flash (Fehlermeldungen) ```css .inline-flash { width: 100%; padding: 0.95rem 1rem; border-radius: 18px; border: 1px solid rgba(220,38,38,0.22); background: rgba(254,242,242,0.92); color: var(--lumo-error-text-color); box-sizing: border-box; } ``` --- ## 32. Checkliste für neue Views Beim Erstellen einer neuen View folgende Punkte prüfen: - [ ] Root-Element hat genau eine View-Klasse (`data-view`, `form-page`, etc.) - [ ] **Kein** `setPadding(true)` auf dem View-Root oder `form-shell`-Wrappern (ohne `form-card`) - [ ] **Kein** `setJustifyContentMode(CENTER)` auf Daten-Views (`data-view`, `admin-form-view`) - [ ] Alle Container haben `max-width: 100%` und `box-sizing: border-box` - [ ] Alle Flex-Container mit wechselnden Inhalten haben `flex-wrap: wrap` - [ ] Formularfelder haben **keine** fixen Pixel-Breiten (`setWidth("200px")` verboten) - [ ] `ViewToolbar` ist das erste Kind-Element - [ ] Grid-Panel verwendet `surface-panel data-grid-panel` mit `setWidthFull()` - [ ] Schmale Formular-Container haben `setWidth("Xpx")` **und** `setMaxWidth("100%")` - [ ] Inline-Styles nur für dynamische Werte verwenden, alles andere per CSS-Klassen - [ ] Sekundäre Buttons (`Abbrechen`, `Zurück`, `Export`) haben `LUMO_TERTIARY`, Hauptaktionen haben `LUMO_PRIMARY` - [ ] Tabbed Dialoge verwenden das `no-inner-card`-Pattern mit innerer weißer Karte statt zusätzlicher Overlay-Dekoration