1411 lines
37 KiB
Markdown
1411 lines
37 KiB
Markdown
# VotianLT – UI Theme & Gestaltungsrichtlinien
|
||
|
||
> Gilt für alle Views unter `src/main/java/de/assecutor/votianlt/pages/view/`
|
||
> Theme-Datei: `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
|
||
└── <geroutete View>
|
||
```
|
||
|
||
### `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<T> 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
|