[FEATURE] PWA + Showcase Grid
CI / build-and-test (pull_request) Successful in 10m36s

This commit is contained in:
Pit Friedrich
2026-07-12 18:10:26 +02:00
parent 9df764d065
commit acb2f5964f
11 changed files with 4771 additions and 93 deletions
+1
View File
@@ -12,3 +12,4 @@
node_modules/ node_modules/
src/main/frontend/generated/ src/main/frontend/generated/
vite.generated.ts vite.generated.ts
/src/main/bundles/prod.bundle
+1 -1
View File
@@ -32,7 +32,7 @@ Vaadin Flow (server-side Java UI, no hand-written HTML/JS for views) on Spring B
Since `apex-chart.ts` renders into light DOM (`createRenderRoot()` returns `this`), global CSS can reach into the chart markup — but series/legend/grid colors are driven entirely by the options JSON, not CSS, because ApexCharts renders its own SVG/canvas. Since `apex-chart.ts` renders into light DOM (`createRenderRoot()` returns `this`), global CSS can reach into the chart markup — but series/legend/grid colors are driven entirely by the options JSON, not CSS, because ApexCharts renders its own SVG/canvas.
**Views** (`views/`): `MainLayout` (`@Layout`, applies to all routes) is the `AppLayout` shell — navbar + `SideNav` drawer. `DashboardView` (`@Route("")`) is the only route; it wraps each chart in a card via a local `card(title, chart)` helper rather than adding charts directly. **Views** (`views/`): `MainLayout` (`@Layout`, applies to all routes) is the `AppLayout` shell — navbar + `SideNav` drawer, with one `SideNavItem` per route. Routes: `DashboardView` (`@Route("")`) wraps each chart in a `Card` (`components/Card.java`); `FormView` (`@Route("formular")`) demonstrates form controls bound via `Binder`; `TableView` (`@Route("tabelle")`) demonstrates a `Grid` over dummy data with a live text filter (`GridListDataView.addFilter`, `TextField` in `ValueChangeMode.EAGER`). New views should reuse `Card` to wrap their content rather than adding components directly, and get a matching `SideNavItem` in `MainLayout`.
**Styling**: `src/main/resources/META-INF/resources/styles.css` is the one project-level stylesheet (loaded via `@StyleSheet("styles.css")` in `Application.java`). It defines `--dialect-*` design tokens (Dialect design system: primary orange `#E86C00`, cool-gray background, card radius/shadow) and aliases them onto Aura's own CSS custom properties (`--aura-accent-color-*`, `--aura-background-color-*`, `--aura-orange`, `--aura-yellow`) rather than fighting the theme. Aura tokens use OKLCH + relative-color syntax and accept plain hex overrides. When restyling, prefer extending this alias layer over hardcoding new colors in components. **Styling**: `src/main/resources/META-INF/resources/styles.css` is the one project-level stylesheet (loaded via `@StyleSheet("styles.css")` in `Application.java`). It defines `--dialect-*` design tokens (Dialect design system: primary orange `#E86C00`, cool-gray background, card radius/shadow) and aliases them onto Aura's own CSS custom properties (`--aura-accent-color-*`, `--aura-background-color-*`, `--aura-orange`, `--aura-yellow`) rather than fighting the theme. Aura tokens use OKLCH + relative-color syntax and accept plain hex overrides. When restyling, prefer extending this alias layer over hardcoding new colors in components.
+4497 -49
View File
File diff suppressed because it is too large Load Diff
+22 -6
View File
@@ -40,7 +40,12 @@
"transform-ast": "2.4.4", "transform-ast": "2.4.4",
"typescript": "6.0.3", "typescript": "6.0.3",
"vite": "8.0.16", "vite": "8.0.16",
"vite-plugin-checker": "0.14.4" "vite-plugin-checker": "0.14.4",
"workbox-build": "7.4.1",
"workbox-core": "7.4.1",
"workbox-precaching": "7.4.1",
"workbox-routing": "7.4.1",
"workbox-strategies": "7.4.1"
}, },
"vaadin": { "vaadin": {
"dependencies": { "dependencies": {
@@ -81,9 +86,14 @@
"transform-ast": "2.4.4", "transform-ast": "2.4.4",
"typescript": "6.0.3", "typescript": "6.0.3",
"vite": "8.0.16", "vite": "8.0.16",
"vite-plugin-checker": "0.14.4" "vite-plugin-checker": "0.14.4",
"workbox-build": "7.4.1",
"workbox-core": "7.4.1",
"workbox-precaching": "7.4.1",
"workbox-routing": "7.4.1",
"workbox-strategies": "7.4.1"
}, },
"hash": "8bfc1d6a9ed865f834789abfbf3b610d4bb660aaee424aefb9e9ae718e11717b", "hash": "c759239a4f6d12f111d0cab12fe3ce6fdb558c5f8e2a3f33425bcfb33577e99b",
"overrides": { "overrides": {
"@vaadin/a11y-base": "25.2.0", "@vaadin/a11y-base": "25.2.0",
"@vaadin/accordion": "25.2.0", "@vaadin/accordion": "25.2.0",
@@ -169,7 +179,10 @@
"proj4": "$proj4", "proj4": "$proj4",
"react": "$react", "react": "$react",
"react-dom": "$react-dom", "react-dom": "$react-dom",
"react-router": "$react-router" "react-router": "$react-router",
"workbox-build": {
"glob": "13.0.6"
}
} }
}, },
"overrides": { "overrides": {
@@ -202,7 +215,6 @@
"@vaadin/progress-bar": "25.2.0", "@vaadin/progress-bar": "25.2.0",
"@vaadin/multi-select-combo-box": "25.2.0", "@vaadin/multi-select-combo-box": "25.2.0",
"proj4": "$proj4", "proj4": "$proj4",
"apexcharts": "$apexcharts",
"@vaadin/crud": "25.2.0", "@vaadin/crud": "25.2.0",
"react-router": "$react-router", "react-router": "$react-router",
"@vaadin/time-picker": "25.2.0", "@vaadin/time-picker": "25.2.0",
@@ -257,6 +269,10 @@
"@vaadin/rich-text-editor": "25.2.0", "@vaadin/rich-text-editor": "25.2.0",
"@vaadin/vaadin-lumo-styles": "$@vaadin/vaadin-lumo-styles", "@vaadin/vaadin-lumo-styles": "$@vaadin/vaadin-lumo-styles",
"@vaadin/email-field": "25.2.0", "@vaadin/email-field": "25.2.0",
"@vaadin/message-input": "25.2.0" "@vaadin/message-input": "25.2.0",
"workbox-build": {
"glob": "13.0.6"
},
"apexcharts": "$apexcharts"
} }
} }
@@ -7,11 +7,13 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.vaadin.flow.component.dependency.StyleSheet; import com.vaadin.flow.component.dependency.StyleSheet;
import com.vaadin.flow.component.page.AppShellConfigurator; import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.component.page.Push; import com.vaadin.flow.component.page.Push;
import com.vaadin.flow.server.PWA;
@SpringBootApplication @SpringBootApplication
@StyleSheet(Aura.STYLESHEET) @StyleSheet(Aura.STYLESHEET)
@StyleSheet("styles.css") // Your custom styles @StyleSheet("styles.css") // Your custom styles
@Push @Push
@PWA(name = "Dialect Charts", shortName = "Dialect", offlinePath = "offline.html")
public class Application implements AppShellConfigurator { public class Application implements AppShellConfigurator {
public static void main(String[] args) { public static void main(String[] args) {
@@ -0,0 +1,20 @@
package com.example.components;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
public class Card extends VerticalLayout {
public Card(String title, Component content) {
H3 heading = new H3(title);
heading.addClassName("dialect-card__title");
add(heading, content);
addClassName("dialect-card");
setWidthFull();
setPadding(false);
setSpacing(false);
}
}
@@ -1,10 +1,9 @@
package com.example.views; package com.example.views;
import com.example.components.BarChart; import com.example.components.BarChart;
import com.example.components.Card;
import com.example.components.LineChart; import com.example.components.LineChart;
import com.example.components.PieChart; import com.example.components.PieChart;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
@@ -51,20 +50,8 @@ public class DashboardView extends VerticalLayout {
Notification.show("Slice %d".formatted(e.getDataPointIndex())); Notification.show("Slice %d".formatted(e.getDataPointIndex()));
}); });
add(card("Umsatz-Entwicklung", lineChart), add(new Card("Umsatz-Entwicklung", lineChart),
card("Umsatz nach Monat", barChart), new Card("Umsatz nach Monat", barChart),
card("Umsatz nach Region", pieChart)); new Card("Umsatz nach Region", pieChart));
}
private Component card(String title, Component chart) {
H3 heading = new H3(title);
heading.addClassName("dialect-card__title");
VerticalLayout card = new VerticalLayout(heading, chart);
card.addClassName("dialect-card");
card.setWidthFull();
card.setPadding(false);
card.setSpacing(false);
return card;
} }
} }
+3 -20
View File
@@ -1,5 +1,6 @@
package com.example.views; package com.example.views;
import com.example.components.Card;
import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
@@ -8,19 +9,13 @@ import com.vaadin.flow.component.checkbox.CheckboxGroup;
import com.vaadin.flow.component.combobox.ComboBox; import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.datepicker.DatePicker; import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant; import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.radiobutton.RadioButtonGroup; import com.vaadin.flow.component.radiobutton.RadioButtonGroup;
import com.vaadin.flow.component.select.Select; import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.textfield.BigDecimalField; import com.vaadin.flow.component.textfield.*;
import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.component.textfield.IntegerField;
import com.vaadin.flow.component.textfield.PasswordField;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.timepicker.TimePicker; import com.vaadin.flow.component.timepicker.TimePicker;
import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.validator.EmailValidator; import com.vaadin.flow.data.validator.EmailValidator;
@@ -45,7 +40,7 @@ public class FormView extends VerticalLayout {
public FormView() { public FormView() {
addClassName("dialect-content"); addClassName("dialect-content");
add(card("Registrierung", buildForm())); add(new Card("Registrierung", buildForm()));
//<theme-editor-local-classname> //<theme-editor-local-classname>
addClassName("form-view-vertical-layout-1"); addClassName("form-view-vertical-layout-1");
} }
@@ -171,16 +166,4 @@ vorname.setAutofocus(true);
wrapper.setSpacing(true); wrapper.setSpacing(true);
return wrapper; return wrapper;
} }
private Component card(String title, Component body) {
H3 heading = new H3(title);
heading.addClassName("dialect-card__title");
VerticalLayout card = new VerticalLayout(heading, body);
card.addClassName("dialect-card");
card.setWidthFull();
card.setPadding(false);
card.setSpacing(false);
return card;
}
} }
@@ -39,6 +39,8 @@ public class MainLayout extends AppLayout {
VaadinIcon.DASHBOARD.create())); VaadinIcon.DASHBOARD.create()));
nav.addItem(new SideNavItem("Formular", FormView.class, nav.addItem(new SideNavItem("Formular", FormView.class,
VaadinIcon.FORM.create())); VaadinIcon.FORM.create()));
nav.addItem(new SideNavItem("Tabelle", TableView.class,
VaadinIcon.TABLE.create()));
addToDrawer(nav); addToDrawer(nav);
} }
@@ -0,0 +1,81 @@
package com.example.views;
import com.example.components.Card;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.dataview.GridListDataView;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import java.text.NumberFormat;
import java.util.List;
import java.util.Locale;
/**
* Example view demonstrating a {@link Grid} with dummy data and a
* live text filter across all columns.
*/
@Route("tabelle")
@PageTitle("Tabelle")
public class TableView extends VerticalLayout {
private record Mitarbeiter(String name, String abteilung, String stadt, double gehalt) {
}
private static final List<Mitarbeiter> MITARBEITER = List.of(
new Mitarbeiter("Anna Schmidt", "Entwicklung", "Berlin", 68000),
new Mitarbeiter("Jonas Becker", "Vertrieb", "Hamburg", 54000),
new Mitarbeiter("Lena Fischer", "Marketing", "München", 59000),
new Mitarbeiter("Paul Wagner", "Support", "Köln", 47000),
new Mitarbeiter("Mia Hoffmann", "Geschäftsführung", "Frankfurt", 95000),
new Mitarbeiter("Felix Bauer", "Entwicklung", "Stuttgart", 71000),
new Mitarbeiter("Sophie Richter", "Vertrieb", "Wien", 52000),
new Mitarbeiter("Tim Klein", "Marketing", "Zürich", 63000),
new Mitarbeiter("Laura Wolf", "Support", "Leipzig", 45000),
new Mitarbeiter("Max Neumann", "Entwicklung", "Dresden", 69000),
new Mitarbeiter("Nina Schröder", "Geschäftsführung", "Düsseldorf", 98000),
new Mitarbeiter("David Zimmermann", "Vertrieb", "Nürnberg", 55000));
private static final NumberFormat GEHALT_FORMAT = NumberFormat.getCurrencyInstance(Locale.GERMANY);
public TableView() {
addClassName("dialect-content");
TextField suche = new TextField();
suche.setPlaceholder("Suchen…");
suche.setPrefixComponent(VaadinIcon.SEARCH.create());
suche.setClearButtonVisible(true);
suche.setValueChangeMode(ValueChangeMode.EAGER);
suche.setWidthFull();
Grid<Mitarbeiter> grid = new Grid<>();
grid.setWidthFull();
grid.addColumn(Mitarbeiter::name).setHeader("Name").setSortable(true);
grid.addColumn(Mitarbeiter::abteilung).setHeader("Abteilung").setSortable(true);
grid.addColumn(Mitarbeiter::stadt).setHeader("Stadt").setSortable(true);
grid.addColumn(m -> GEHALT_FORMAT.format(m.gehalt())).setHeader("Gehalt (€)").setSortable(true);
GridListDataView<Mitarbeiter> dataView = grid.setItems(MITARBEITER);
suche.addValueChangeListener(e -> dataView.refreshAll());
dataView.addFilter(mitarbeiter -> matches(mitarbeiter, suche.getValue()));
VerticalLayout content = new VerticalLayout(suche, grid);
content.setPadding(false);
content.setSpacing(true);
add(new Card("Mitarbeiter", content));
}
private static boolean matches(Mitarbeiter mitarbeiter, String filter) {
if (filter == null || filter.isBlank()) {
return true;
}
String suchbegriff = filter.trim().toLowerCase(Locale.GERMANY);
return mitarbeiter.name().toLowerCase(Locale.GERMANY).contains(suchbegriff)
|| mitarbeiter.abteilung().toLowerCase(Locale.GERMANY).contains(suchbegriff)
|| mitarbeiter.stadt().toLowerCase(Locale.GERMANY).contains(suchbegriff);
}
}
@@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Keine Verbindung — Dialect Charts</title>
<style>
:root {
color-scheme: light dark;
--dialect-primary: #e86c00;
--dialect-secondary: #f6b93b;
--dialect-bg: light-dark(#f4f6f9, #14171f);
--dialect-surface: light-dark(#ffffff, #1c2029);
--dialect-ink: light-dark(#14171f, #e6eaf0);
--dialect-ink-muted: light-dark(#5b6472, #9aa3b2);
--dialect-border: light-dark(#e6eaf0, #2b303b);
--dialect-radius: 14px;
--dialect-shadow-1: light-dark(0 6px 20px rgba(20, 23, 31, 0.08), 0 6px 20px rgba(0, 0, 0, 0.35));
--dialect-shadow-2: light-dark(0 1px 2px rgba(20, 23, 31, 0.06), 0 1px 2px rgba(0, 0, 0, 0.3));
}
* { box-sizing: border-box; }
html, body {
height: 100%;
margin: 0;
}
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 24px;
background: var(--dialect-bg);
color: var(--dialect-ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.card {
width: 100%;
max-width: 420px;
background: var(--dialect-surface);
border-radius: var(--dialect-radius);
box-shadow: var(--dialect-shadow-1), var(--dialect-shadow-2);
padding: 40px 32px;
text-align: center;
}
.icon {
width: 56px;
height: 56px;
margin: 0 auto 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: color-mix(in srgb, var(--dialect-primary) 14%, transparent);
color: var(--dialect-primary);
}
.icon svg {
width: 28px;
height: 28px;
}
h1 {
margin: 0 0 10px;
font-size: 1.3rem;
font-weight: 600;
letter-spacing: -0.01em;
}
p {
margin: 0 0 28px;
color: var(--dialect-ink-muted);
font-size: 0.95rem;
line-height: 1.5;
}
button {
appearance: none;
border: none;
border-radius: 8px;
padding: 10px 22px;
font-size: 0.95rem;
font-weight: 600;
color: #fff;
background: var(--dialect-primary);
cursor: pointer;
}
button:hover {
background: color-mix(in srgb, var(--dialect-primary) 88%, black);
}
.hint {
margin-top: 18px;
margin-bottom: 0;
font-size: 0.8rem;
color: var(--dialect-ink-muted);
}
.hint::before {
content: "";
display: inline-block;
width: 6px;
height: 6px;
margin-right: 6px;
border-radius: 999px;
background: var(--dialect-secondary);
}
</style>
</head>
<body>
<div class="card">
<div class="icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 9a17 17 0 0 1 22 0"></path>
<path d="M5 13a11 11 0 0 1 14 0"></path>
<path d="M9 17a5 5 0 0 1 6 0"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>
</div>
<h1>Keine Verbindung</h1>
<p>Dialect Charts ist derzeit offline. Bitte prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.</p>
<button id="retry-button">Erneut versuchen</button>
<p class="hint">Die Seite wird automatisch neu geladen, sobald die Verbindung wiederhergestellt ist.</p>
</div>
<script>
function reconnect() {
location.reload();
}
document.getElementById('retry-button').addEventListener('click', reconnect);
window.addEventListener('online', reconnect);
</script>
</body>
</html>