[FEATURE] Customn Theme
CI / build-and-test (pull_request) Successful in 10m26s

This commit is contained in:
Pit Friedrich
2026-07-07 19:55:42 +02:00
parent 5ed6937c88
commit d947833214
9 changed files with 528 additions and 12 deletions
@@ -10,11 +10,13 @@ public abstract class AxisChart extends ApexChart {
}
public void setData(String seriesName, List<Double> values, List<String> categories) {
Map<String, Object> options = Map.of(
"chart", Map.of("type", getChartType(), "height", "100%"),
"series", List.of(Map.of("name", seriesName, "data", values)),
"xaxis", Map.of("categories", categories)
);
Map<String, Object> options = DialectTheme.baseOptions(getChartType());
options.put("series", List.of(Map.of("name", seriesName, "data", values)));
options.put("xaxis", Map.of(
"categories", categories,
"axisBorder", Map.of("show", false),
"axisTicks", Map.of("show", false)
));
sendOptions(options);
}
@@ -0,0 +1,82 @@
package com.example.components;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Shared ApexCharts styling for the Dialect design system
* (https://www.figma.com/design/6DRThj0vmRgfjMIdsmkaIy).
*/
public final class DialectTheme {
/** Categorical palette, primary orange first. */
public static final List<String> COLORS = List.of(
"#E86C00", // primary orange
"#4C9BE8", // blue
"#2FB7A6", // teal
"#F6B93B", // gold
"#8E7CD6", // purple
"#E5544B", // red
"#3F51B5" // indigo
);
private static final String FONT_FAMILY =
"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
/**
* Mirrors {@code --dialect-border} in styles.css. Charts render via JS/SVG,
* not CSS, so this can't read the custom property directly — keep both in sync
* by hand if the token changes.
*/
private static final String BORDER_COLOR = "#E6EAF0";
/** Mirrors {@code --dialect-ink} in styles.css. See {@link #BORDER_COLOR} note. */
private static final String INK_COLOR = "#14171F";
private DialectTheme() {
}
/** Common option fragments merged into every chart before series/labels are added. */
public static Map<String, Object> baseOptions(String chartType) {
Map<String, Object> options = new LinkedHashMap<>();
Map<String, Object> chart = new LinkedHashMap<>();
chart.put("type", chartType);
chart.put("height", "100%");
chart.put("fontFamily", FONT_FAMILY);
chart.put("toolbar", Map.of("show", false));
options.put("chart", chart);
options.put("colors", COLORS);
options.put("grid", Map.of(
"borderColor", BORDER_COLOR,
"strokeDashArray", 4
));
options.put("legend", Map.of(
"position", "bottom",
"labels", Map.of("colors", INK_COLOR)
));
options.put("dataLabels", Map.of("enabled", false));
options.put("tooltip", Map.of("theme", "light"));
if ("line".equals(chartType)) {
options.put("stroke", Map.of("curve", "smooth", "width", 3));
} else if ("bar".equals(chartType)) {
options.put("stroke", Map.of("width", 0));
options.put("plotOptions", Map.of(
"bar", Map.of(
"borderRadius", 6,
"borderRadiusApplication", "end",
"columnWidth", "55%"
)
));
}
return options;
}
}
@@ -10,11 +10,9 @@ public class PieChart extends ApexChart {
}
public void setData(List<Double> values, List<String> labels) {
Map<String, Object> options = Map.of(
"chart", Map.of("type", "pie", "height", "100%"),
"series", values,
"labels", labels
);
Map<String, Object> options = DialectTheme.baseOptions("pie");
options.put("series", values);
options.put("labels", labels);
sendOptions(options);
}
@@ -3,16 +3,22 @@ package com.example.views;
import com.example.components.BarChart;
import com.example.components.LineChart;
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.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import java.util.List;
@Route("")
@PageTitle("Dashboard")
public class DashboardView extends VerticalLayout {
public DashboardView() {
addClassName("dialect-content");
LineChart lineChart = new LineChart();
lineChart.setWidthFull();
lineChart.setHeight("400px");
@@ -45,6 +51,20 @@ public class DashboardView extends VerticalLayout {
Notification.show("Slice %d".formatted(e.getDataPointIndex()));
});
add(lineChart, barChart, pieChart);
add(card("Umsatz-Entwicklung", lineChart),
card("Umsatz nach Monat", barChart),
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;
}
}
@@ -0,0 +1,161 @@
package com.example.views;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.checkbox.CheckboxGroup;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.datepicker.DatePicker;
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.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.radiobutton.RadioButtonGroup;
import com.vaadin.flow.component.select.Select;
import com.vaadin.flow.component.textfield.BigDecimalField;
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.data.binder.Binder;
import com.vaadin.flow.data.validator.EmailValidator;
import com.vaadin.flow.data.validator.IntegerRangeValidator;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
/**
* Example view demonstrating a broad set of Vaadin form controls bound
* to {@link Registration} via {@link Binder}.
*/
@Route("formular")
@PageTitle("Formular")
public class FormView extends VerticalLayout {
private final Binder<Registration> binder = new Binder<>(Registration.class);
public FormView() {
addClassName("dialect-content");
add(card("Registrierung", buildForm()));
}
private Component buildForm() {
TextField vorname = new TextField("Vorname");
TextField nachname = new TextField("Nachname");
EmailField email = new EmailField("E-Mail");
PasswordField passwort = new PasswordField("Passwort");
IntegerField alter = new IntegerField("Alter");
BigDecimalField gehalt = new BigDecimalField("Gehalt (€)");
DatePicker geburtsdatum = new DatePicker("Geburtsdatum");
TimePicker uhrzeit = new TimePicker("Bevorzugte Uhrzeit");
ComboBox<String> land = new ComboBox<>("Land");
land.setItems("Deutschland", "Österreich", "Schweiz", "Frankreich", "Niederlande");
Select<String> abteilung = new Select<>();
abteilung.setLabel("Abteilung");
abteilung.setItems("Vertrieb", "Marketing", "Entwicklung", "Support", "Geschäftsführung");
CheckboxGroup<String> interessen = new CheckboxGroup<>();
interessen.setLabel("Interessen");
interessen.setItems("Newsletter-Themen", "Webinare", "Produkt-Updates", "Events");
RadioButtonGroup<String> geschlecht = new RadioButtonGroup<>();
geschlecht.setLabel("Anrede");
geschlecht.setItems("Frau", "Herr", "Divers", "Keine Angabe");
Checkbox newsletter = new Checkbox("Newsletter abonnieren");
TextArea bemerkung = new TextArea("Bemerkung");
bemerkung.setMaxLength(500);
binder.forField(vorname)
.asRequired("Vorname wird benötigt")
.bind(Registration::getVorname, Registration::setVorname);
binder.forField(nachname)
.asRequired("Nachname wird benötigt")
.bind(Registration::getNachname, Registration::setNachname);
binder.forField(email)
.asRequired("E-Mail wird benötigt")
.withValidator(new EmailValidator("Ungültige E-Mail-Adresse"))
.bind(Registration::getEmail, Registration::setEmail);
binder.forField(passwort)
.bind(Registration::getPasswort, Registration::setPasswort);
binder.forField(alter)
.withValidator(new IntegerRangeValidator("Alter muss zwischen 0 und 120 liegen", 0, 120))
.bind(Registration::getAlter, Registration::setAlter);
binder.forField(gehalt)
.bind(Registration::getGehalt, Registration::setGehalt);
binder.forField(geburtsdatum)
.bind(Registration::getGeburtsdatum, Registration::setGeburtsdatum);
binder.forField(uhrzeit)
.bind(Registration::getUhrzeit, Registration::setUhrzeit);
binder.forField(land)
.bind(Registration::getLand, Registration::setLand);
binder.forField(abteilung)
.bind(Registration::getAbteilung, Registration::setAbteilung);
binder.forField(interessen)
.bind(Registration::getInteressen, Registration::setInteressen);
binder.forField(geschlecht)
.bind(Registration::getGeschlecht, Registration::setGeschlecht);
binder.forField(newsletter)
.bind(Registration::isNewsletter, Registration::setNewsletter);
binder.forField(bemerkung)
.bind(Registration::getBemerkung, Registration::setBemerkung);
Registration registration = new Registration();
binder.readBean(registration);
FormLayout formLayout = new FormLayout();
formLayout.setResponsiveSteps(
new FormLayout.ResponsiveStep("0", 1),
new FormLayout.ResponsiveStep("500px", 2));
formLayout.add(vorname, nachname, email, passwort, alter, gehalt,
geburtsdatum, uhrzeit, land, abteilung);
formLayout.setColspan(interessen, 2);
formLayout.setColspan(geschlecht, 2);
formLayout.setColspan(newsletter, 2);
formLayout.setColspan(bemerkung, 2);
formLayout.add(interessen, geschlecht, newsletter, bemerkung);
Button speichern = new Button("Speichern", e -> {
Registration ziel = new Registration();
if (binder.writeBeanIfValid(ziel)) {
Notification erfolg = Notification.show(
"Gespeichert: %s %s".formatted(ziel.getVorname(), ziel.getNachname()));
erfolg.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
} else {
Notification fehler = Notification.show("Bitte Eingaben prüfen");
fehler.addThemeVariants(NotificationVariant.LUMO_ERROR);
}
});
speichern.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button zuruecksetzen = new Button("Zurücksetzen",
e -> binder.readBean(new Registration()));
HorizontalLayout buttons = new HorizontalLayout(speichern, zuruecksetzen);
VerticalLayout wrapper = new VerticalLayout(formLayout, buttons);
wrapper.setPadding(false);
wrapper.setSpacing(true);
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;
}
}
@@ -0,0 +1,30 @@
package com.example.views;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.applayout.DrawerToggle;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.sidenav.SideNav;
import com.vaadin.flow.component.sidenav.SideNavItem;
import com.vaadin.flow.router.Layout;
@Layout
public class MainLayout extends AppLayout {
public MainLayout() {
DrawerToggle toggle = new DrawerToggle();
Span title = new Span("Chart App");
title.addClassName("dialect-title");
addToNavbar(toggle, title);
SideNav nav = new SideNav();
nav.addItem(new SideNavItem("Dashboard", DashboardView.class,
VaadinIcon.DASHBOARD.create()));
nav.addItem(new SideNavItem("Formular", FormView.class,
VaadinIcon.FORM.create()));
addToDrawer(nav);
}
}
@@ -0,0 +1,142 @@
package com.example.views;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.HashSet;
import java.util.Set;
/**
* Plain POJO backing the example form on {@link FormView}. Bound via
* {@link com.vaadin.flow.data.binder.Binder} — one field per showcased
* Vaadin control type.
*/
public class Registration {
private String vorname;
private String nachname;
private String email;
private String passwort;
private Integer alter;
private BigDecimal gehalt;
private LocalDate geburtsdatum;
private LocalTime uhrzeit;
private String land;
private String abteilung;
private Set<String> interessen = new HashSet<>();
private String geschlecht;
private boolean newsletter;
private String bemerkung;
public String getVorname() {
return vorname;
}
public void setVorname(String vorname) {
this.vorname = vorname;
}
public String getNachname() {
return nachname;
}
public void setNachname(String nachname) {
this.nachname = nachname;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPasswort() {
return passwort;
}
public void setPasswort(String passwort) {
this.passwort = passwort;
}
public Integer getAlter() {
return alter;
}
public void setAlter(Integer alter) {
this.alter = alter;
}
public BigDecimal getGehalt() {
return gehalt;
}
public void setGehalt(BigDecimal gehalt) {
this.gehalt = gehalt;
}
public LocalDate getGeburtsdatum() {
return geburtsdatum;
}
public void setGeburtsdatum(LocalDate geburtsdatum) {
this.geburtsdatum = geburtsdatum;
}
public LocalTime getUhrzeit() {
return uhrzeit;
}
public void setUhrzeit(LocalTime uhrzeit) {
this.uhrzeit = uhrzeit;
}
public String getLand() {
return land;
}
public void setLand(String land) {
this.land = land;
}
public String getAbteilung() {
return abteilung;
}
public void setAbteilung(String abteilung) {
this.abteilung = abteilung;
}
public Set<String> getInteressen() {
return interessen;
}
public void setInteressen(Set<String> interessen) {
this.interessen = interessen;
}
public String getGeschlecht() {
return geschlecht;
}
public void setGeschlecht(String geschlecht) {
this.geschlecht = geschlecht;
}
public boolean isNewsletter() {
return newsletter;
}
public void setNewsletter(boolean newsletter) {
this.newsletter = newsletter;
}
public String getBemerkung() {
return bemerkung;
}
public void setBemerkung(String bemerkung) {
this.bemerkung = bemerkung;
}
}
@@ -1 +1,43 @@
/* Add your styles here */
/* Dialect design system tokens (https://www.figma.com/design/6DRThj0vmRgfjMIdsmkaIy) */
:root {
--dialect-primary: #e86c00;
--dialect-secondary: #f6b93b;
--dialect-bg: #f4f6f9;
--dialect-surface: #ffffff;
--dialect-ink: #14171f;
--dialect-border: #e6eaf0;
--dialect-radius: 14px;
--dialect-shadow: 0 6px 20px rgba(20, 23, 31, 0.08), 0 1px 2px rgba(20, 23, 31, 0.06);
/* Alias onto Aura's theming hooks */
--aura-accent-color-light: var(--dialect-primary);
--aura-accent-color-dark: var(--dialect-primary);
--aura-background-color-light: var(--dialect-bg);
--aura-orange: var(--dialect-primary);
--aura-yellow: var(--dialect-secondary);
}
.dialect-title {
font-weight: 600;
color: var(--dialect-ink);
}
.dialect-content {
max-width: 960px;
margin: 0 auto;
padding: var(--lumo-space-l, 24px);
gap: 24px;
}
.dialect-card {
background: var(--dialect-surface);
border-radius: var(--dialect-radius);
box-shadow: var(--dialect-shadow);
padding: 20px;
}
.dialect-card__title {
font-weight: 600;
color: var(--dialect-ink);
margin: 0 0 12px 0;
}