Demo application to show the contrast problem with warning messages using the Lumo theme of Vaadin.

Signed-off-by: Marcus Fihlon <marcus@fihlon.swiss>
This commit is contained in:
Marcus Fihlon 2025-09-25 10:53:03 +02:00
parent 6317f9e12f
commit 6679eada69
Signed by: McPringle
GPG key ID: C6B7F469EE363E1F
16 changed files with 105 additions and 458 deletions

View file

@ -1,86 +1 @@
# App README Demo application to show the contrast problem with warning messages using the Lumo theme of Vaadin.
- [ ] TODO Replace or update this README with instructions relevant to your application
## Project Structure
The sources of your App have the following structure:
```
src
├── main/frontend
│ └── themes
│ └── default
│ ├── styles.css
│ └── theme.json
├── main/java
│ └── [application package]
│ ├── base
│ │ └── ui
│ │ ├── component
│ │ │ └── ViewToolbar.java
│ │ ├── MainErrorHandler.java
│ │ └── MainLayout.java
│ ├── examplefeature
│ │ ├── ui
│ │ │ └── TaskListView.java
│ │ ├── Task.java
│ │ ├── TaskRepository.java
│ │ └── TaskService.java
│ └── Application.java
└── test/java
└── [application package]
└── examplefeature
└── TaskServiceTest.java
```
The main entry point into the application is `Application.java`. This class contains the `main()` method that start up
the Spring Boot application.
The skeleton follows a *feature-based package structure*, organizing code by *functional units* rather than traditional
architectural layers. It includes two feature packages: `base` and `examplefeature`.
* The `base` package contains classes meant for reuse across different features, either through composition or
inheritance. You can use them as-is, tweak them to your needs, or remove them.
* The `examplefeature` package is an example feature package that demonstrates the structure. It represents a
*self-contained unit of functionality*, including UI components, business logic, data access, and an integration test.
Once you create your own features, *you'll remove this package*.
The `src/main/frontend` directory contains an empty theme called `default`, based on the Lumo theme. It is activated in
the `Application` class, using the `@Theme` annotation.
## Starting in Development Mode
To start the application in development mode, import it into your IDE and run the `Application` class.
You can also start the application from the command line by running:
```bash
./mvnw
```
## Building for Production
To build the application in production mode, run:
```bash
./mvnw -Pproduction package
```
To build a Docker image, run:
```bash
docker build -t my-application:latest .
```
If you use commercial components, pass the license key as a build secret:
```bash
docker build --secret id=proKey,src=$HOME/.vaadin/proKey .
```
## Getting Started
The [Getting Started](https://vaadin.com/docs/latest/getting-started) guide will quickly familiarize you with your new
App implementation. You'll learn how to set up your development environment, understand the project
structure, and find resources to help you add muscles to your skeleton — transforming it into a fully-featured
application.

0
mvnw vendored Executable file → Normal file
View file

View file

@ -0,0 +1,32 @@
This directory is automatically generated by Vaadin and contains the pre-compiled
frontend files/resources for your project (frontend development bundle).
It should be added to Version Control System and committed, so that other developers
do not have to compile it again.
Frontend development bundle is automatically updated when needed:
- an npm/pnpm package is added with @NpmPackage or directly into package.json
- CSS, JavaScript or TypeScript files are added with @CssImport, @JsModule or @JavaScript
- Vaadin add-on with front-end customizations is added
- Custom theme imports/assets added into 'theme.json' file
- Exported web component is added.
If your project development needs a hot deployment of the frontend changes,
you can switch Flow to use Vite development server (default in Vaadin 23.3 and earlier versions):
- set `vaadin.frontend.hotdeploy=true` in `application.properties`
- configure `vaadin-maven-plugin`:
```
<configuration>
<frontendHotdeploy>true</frontendHotdeploy>
</configuration>
```
- configure `jetty-maven-plugin`:
```
<configuration>
<systemProperties>
<vaadin.frontend.hotdeploy>true</vaadin.frontend.hotdeploy>
</systemProperties>
</configuration>
```
Read more [about Vaadin development mode](https://vaadin.com/docs/next/flow/configuration/development-mode#precompiled-bundle).

BIN
src/main/bundles/dev.bundle Normal file

Binary file not shown.

View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<!--
This file is auto-generated by Vaadin.
-->
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<style>
html, body, #outlet {
height: 100%;
width: 100%;
margin: 0;
}
</style>
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
</head>
<body>
<!-- This outlet div is where the views are rendered -->
<div id="outlet"></div>
</body>
</html>

View file

@ -1 +1,16 @@
/* Add your styles here */ /* Add your styles here */
.warning {
display: flex;
justify-content: center;
justify-items: center;
background-color: var(--lumo-warning-color);
color: var(--lumo-warning-text-color);
box-shadow: var(--lumo-box-shadow-s);
font-size: var(--lumo-font-size-xl);
}
h1 {
font-size: var(--lumo-font-size-l);
padding-top: var(--lumo-space-l);
}

View file

@ -1,9 +1,4 @@
{ {
"lumoImports": [ "lumoImports" : [ "typography", "color", "spacing", "badge", "utility" ],
"typography", "autoInjectComponents" : "false"
"color", }
"spacing",
"badge",
"utility"
]
}

View file

@ -1,32 +0,0 @@
package com.example.base.ui;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.server.VaadinServiceInitListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class MainErrorHandler {
private static final Logger log = LoggerFactory.getLogger(MainErrorHandler.class);
@Bean
public VaadinServiceInitListener errorHandlerInitializer() {
return (event) -> event.getSource().addSessionInitListener(
sessionInitEvent -> sessionInitEvent.getSession().setErrorHandler(errorEvent -> {
log.error("An unexpected error occurred", errorEvent.getThrowable());
errorEvent.getComponent().flatMap(Component::getUI).ifPresent(ui -> {
var notification = new Notification(
"An unexpected error has occurred. Please try again later.");
notification.addThemeVariants(NotificationVariant.LUMO_ERROR);
notification.setPosition(Notification.Position.TOP_CENTER);
notification.setDuration(3000);
ui.access(notification::open);
});
}));
}
}

View file

@ -1,52 +0,0 @@
package com.example.base.ui;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.Scroller;
import com.vaadin.flow.component.sidenav.SideNav;
import com.vaadin.flow.component.sidenav.SideNavItem;
import com.vaadin.flow.router.Layout;
import com.vaadin.flow.server.menu.MenuConfiguration;
import com.vaadin.flow.server.menu.MenuEntry;
import static com.vaadin.flow.theme.lumo.LumoUtility.*;
@Layout
public final class MainLayout extends AppLayout {
MainLayout() {
setPrimarySection(Section.DRAWER);
addToDrawer(createHeader(), new Scroller(createSideNav()));
}
private Div createHeader() {
// TODO Replace with real application logo and name
var appLogo = VaadinIcon.CUBES.create();
appLogo.addClassNames(TextColor.PRIMARY, IconSize.LARGE);
var appName = new Span("App");
appName.addClassNames(FontWeight.SEMIBOLD, FontSize.LARGE);
var header = new Div(appLogo, appName);
header.addClassNames(Display.FLEX, Padding.MEDIUM, Gap.MEDIUM, AlignItems.CENTER);
return header;
}
private SideNav createSideNav() {
var nav = new SideNav();
nav.addClassNames(Margin.Horizontal.MEDIUM);
MenuConfiguration.getMenuEntries().forEach(entry -> nav.addItem(createSideNavItem(entry)));
return nav;
}
private SideNavItem createSideNavItem(MenuEntry menuEntry) {
if (menuEntry.icon() != null) {
return new SideNavItem(menuEntry.title(), menuEntry.path(), new Icon(menuEntry.icon()));
} else {
return new SideNavItem(menuEntry.title(), menuEntry.path());
}
}
}

View file

@ -1,44 +0,0 @@
package com.example.base.ui.component;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Composite;
import com.vaadin.flow.component.applayout.DrawerToggle;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.component.html.Header;
import com.vaadin.flow.theme.lumo.LumoUtility.*;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public final class ViewToolbar extends Composite<Header> {
public ViewToolbar(@Nullable String viewTitle, Component... components) {
addClassNames(Display.FLEX, FlexDirection.COLUMN, JustifyContent.BETWEEN, AlignItems.STRETCH, Gap.MEDIUM,
FlexDirection.Breakpoint.Medium.ROW, AlignItems.Breakpoint.Medium.CENTER);
var drawerToggle = new DrawerToggle();
drawerToggle.addClassNames(Margin.NONE);
var title = new H1(viewTitle);
title.addClassNames(FontSize.XLARGE, Margin.NONE, FontWeight.LIGHT);
var toggleAndTitle = new Div(drawerToggle, title);
toggleAndTitle.addClassNames(Display.FLEX, AlignItems.CENTER);
getContent().add(toggleAndTitle);
if (components.length > 0) {
var actions = new Div(components);
actions.addClassNames(Display.FLEX, FlexDirection.COLUMN, JustifyContent.BETWEEN, Flex.GROW, Gap.SMALL,
FlexDirection.Breakpoint.Medium.ROW);
getContent().add(actions);
}
}
public static Component group(Component... components) {
var group = new Div(components);
group.addClassNames(Display.FLEX, FlexDirection.COLUMN, AlignItems.STRETCH, Gap.SMALL,
FlexDirection.Breakpoint.Medium.ROW, AlignItems.Breakpoint.Medium.CENTER);
return group;
}
}

View file

@ -1,86 +0,0 @@
package com.example.examplefeature;
import jakarta.persistence.*;
import org.jspecify.annotations.Nullable;
import java.time.Instant;
import java.time.LocalDate;
@Entity
@Table(name = "task")
public class Task {
public static final int DESCRIPTION_MAX_LENGTH = 300;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "task_id")
private Long id;
@Column(name = "description", nullable = false, length = DESCRIPTION_MAX_LENGTH)
private String description = "";
@Column(name = "creation_date", nullable = false)
private Instant creationDate;
@Column(name = "due_date")
@Nullable
private LocalDate dueDate;
protected Task() { // To keep Hibernate happy
}
public Task(String description, Instant creationDate) {
setDescription(description);
this.creationDate = creationDate;
}
public @Nullable Long getId() {
return id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
if (description.length() > DESCRIPTION_MAX_LENGTH) {
throw new IllegalArgumentException("Description length exceeds " + DESCRIPTION_MAX_LENGTH);
}
this.description = description;
}
public Instant getCreationDate() {
return creationDate;
}
public @Nullable LocalDate getDueDate() {
return dueDate;
}
public void setDueDate(@Nullable LocalDate dueDate) {
this.dueDate = dueDate;
}
@Override
public boolean equals(Object obj) {
if (obj == null || !getClass().isAssignableFrom(obj.getClass())) {
return false;
}
if (obj == this) {
return true;
}
Task other = (Task) obj;
return getId() != null && getId().equals(other.getId());
}
@Override
public int hashCode() {
// Hashcode should never change during the lifetime of an object. Because of
// this we can't use getId() to calculate the hashcode. Unless you have sets
// with lots of entities in them, returning the same hashcode should not be a
// problem.
return getClass().hashCode();
}
}

View file

@ -1,13 +0,0 @@
package com.example.examplefeature;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
interface TaskRepository extends JpaRepository<Task, Long>, JpaSpecificationExecutor<Task> {
// If you don't need a total row count, Slice is better than Page as it only performs a select query.
// Page performs both a select and a count query.
Slice<Task> findAllBy(Pageable pageable);
}

View file

@ -1,36 +0,0 @@
package com.example.examplefeature;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.LocalDate;
import java.util.List;
@Service
public class TaskService {
private final TaskRepository taskRepository;
TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}
@Transactional
public void createTask(String description, @Nullable LocalDate dueDate) {
if ("fail".equals(description)) {
throw new RuntimeException("This is for testing the error handler");
}
var task = new Task(description, Instant.now());
task.setDueDate(dueDate);
taskRepository.saveAndFlush(task);
}
@Transactional(readOnly = true)
public List<Task> list(Pageable pageable) {
return taskRepository.findAllBy(pageable).toList();
}
}

View file

@ -1,18 +0,0 @@
/**
* This is a feature package for the Task Management sample feature. Its purpose is to demonstrate how you typically
* structure Vaadin business applications, and how the different building blocks interact.
* <p>
* A feature package represents a self-contained unit of functionality, including UI components, business logic, and
* data access. It could be a subdomain or bounded context (e.g., "Billing"), a specific use case (e.g., "User
* Registration"), or even a complex UI view (e.g., "Dashboard").
* </p>
* <p>
* If your application is very small, you may not need dedicated feature packages. In that case, move the subpackages
* directly to the application package.
* </p>
*/
@NullMarked
package com.example.examplefeature;
// TODO Remove this package once you have added real features
import org.jspecify.annotations.NullMarked;

View file

@ -1,83 +0,0 @@
package com.example.examplefeature.ui;
import com.example.base.ui.component.ViewToolbar;
import com.example.examplefeature.Task;
import com.example.examplefeature.TaskService;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Main;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.Menu;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.theme.lumo.LumoUtility;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Optional;
import static com.vaadin.flow.spring.data.VaadinSpringDataHelpers.toSpringPageRequest;
@Route("")
@PageTitle("Task List")
@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Task List")
class TaskListView extends Main {
private final TaskService taskService;
final TextField description;
final DatePicker dueDate;
final Button createBtn;
final Grid<Task> taskGrid;
TaskListView(TaskService taskService) {
this.taskService = taskService;
description = new TextField();
description.setPlaceholder("What do you want to do?");
description.setAriaLabel("Task description");
description.setMaxLength(Task.DESCRIPTION_MAX_LENGTH);
description.setMinWidth("20em");
dueDate = new DatePicker();
dueDate.setPlaceholder("Due date");
dueDate.setAriaLabel("Due date");
createBtn = new Button("Create", event -> createTask());
createBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
var dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(getLocale())
.withZone(ZoneId.systemDefault());
var dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(getLocale());
taskGrid = new Grid<>();
taskGrid.setItems(query -> taskService.list(toSpringPageRequest(query)).stream());
taskGrid.addColumn(Task::getDescription).setHeader("Description");
taskGrid.addColumn(task -> Optional.ofNullable(task.getDueDate()).map(dateFormatter::format).orElse("Never"))
.setHeader("Due Date");
taskGrid.addColumn(task -> dateTimeFormatter.format(task.getCreationDate())).setHeader("Creation Date");
taskGrid.setSizeFull();
setSizeFull();
addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN,
LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL);
add(new ViewToolbar("Task List", ViewToolbar.group(description, dueDate, createBtn)));
add(taskGrid);
}
private void createTask() {
taskService.createTask(description.getValue(), dueDate.getValue());
taskGrid.getDataProvider().refreshAll();
description.clear();
dueDate.clear();
Notification.show("Task added", 3000, Notification.Position.BOTTOM_END)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
}
}

View file

@ -0,0 +1,31 @@
package com.example.views;
import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H1;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.router.RouterLayout;
@Route("")
public class TestView extends Div implements RouterLayout {
public TestView() {
final var warning = new Div("This is a warning message.");
warning.setClassName("warning");
add(warning);
add(new H1("Lumo Theme Test"));
final var darkModeSwitcher = new Checkbox("Dark mode");
add(darkModeSwitcher);
darkModeSwitcher.addValueChangeListener(event -> {
if (event.getValue()) {
getElement().getThemeList().add("dark");
} else {
getElement().getThemeList().remove("dark");
}
});
}
}