Add custom Solstice theme with a dark/light toggle
CI / Build & Test (pull_request) Successful in 1m6s
CI / Build & Test (pull_request) Successful in 1m6s
Introduces a custom Aura theme variant (amber daylight / aurora cyan midnight, via -light/-dark token pairs) and a header toggle button to switch between them at runtime, using the Vaadin 25 ColorScheme API (Page#setColorScheme, @ColorScheme) rather than the older Lumo-only theme-attribute pattern. The choice is persisted in a cookie so it survives reloads and future sessions without any client-side script, and is restored flash-free on the next visit via an IndexHtmlRequestListener. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BNL5syWGwM7KnFcuZeEKQ1
This commit is contained in:
@@ -39,7 +39,9 @@ Port defaults to 8080 (override via `server.port` in `src/main/resources/applica
|
|||||||
|
|
||||||
**Schema management**: `spring.jpa.hibernate.ddl-auto=update` is used for local dev convenience only. This is explicitly not appropriate for production — the app is meant to move to Flyway (or similar) for real schema migrations before shipping.
|
**Schema management**: `spring.jpa.hibernate.ddl-auto=update` is used for local dev convenience only. This is explicitly not appropriate for production — the app is meant to move to Flyway (or similar) for real schema migrations before shipping.
|
||||||
|
|
||||||
**Frontend theming**: `Application` sets the app-shell config (`@Push`, `Aura` theme via `@StyleSheet(Aura.STYLESHEET)`, plus custom `styles.css`/`view-title.css` under `src/main/resources/META-INF/resources/`). No separate frontend build step is needed for typical UI work since Flow generates the client bundle; the `vaadin-maven-plugin` `build-frontend` goal runs as part of the Maven build.
|
**Frontend theming**: `Application` sets the app-shell config (`@Push`, `Aura` theme via `@StyleSheet(Aura.STYLESHEET)`, plus custom `styles.css`/`view-title.css`/`solstice.css` under `src/main/resources/META-INF/resources/`). No separate frontend build step is needed for typical UI work since Flow generates the client bundle; the `vaadin-maven-plugin` `build-frontend` goal runs as part of the Maven build.
|
||||||
|
|
||||||
|
**Custom theme ("Solstice") and dark/light mode**: `solstice.css` overrides Aura's `-light`/`-dark` suffixed design tokens (e.g. `--aura-accent-color-light/-dark`, `--aura-background-color-light/-dark`) rather than the unsuffixed ones — those are read-only, computed via the native CSS `light-dark()` function from the pair. Everything else should keep consuming the unsuffixed tokens (`--aura-accent-color`, `--vaadin-text-color`, ...) so it follows whichever scheme is active automatically. Switching schemes at runtime uses the Vaadin 25 `ColorScheme` API (`Application` declares `@ColorScheme(ColorScheme.Value.SYSTEM)` as the default; `Page#setColorScheme(...)` changes it at runtime) — do not use the older Lumo-only `setAttribute("theme", ...)` pattern. The feature lives in `com.example.base.ui`: `ThemePreference` (session-scoped) holds the current choice and applies it; `ThemeToggle` is the header button; `ThemeInitializer` restores the choice from a cookie (`ThemeCookie`) on every request/UI-init, both for a flash-free reload and to seed a fresh session, since a plain `@ColorScheme` annotation can't read per-visitor state on its own.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,16 @@ 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.ColorScheme;
|
||||||
import com.vaadin.flow.component.page.Push;
|
import com.vaadin.flow.component.page.Push;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@StyleSheet(Aura.STYLESHEET)
|
@StyleSheet(Aura.STYLESHEET)
|
||||||
@StyleSheet("styles.css") // Your custom styles
|
@StyleSheet("styles.css") // Your custom styles
|
||||||
|
@StyleSheet("solstice.css") // Custom Solstice theme: amber daylight / aurora midnight
|
||||||
|
// Default for first-ever visits, before a preference cookie exists (see ThemeInitializer):
|
||||||
|
// follow the OS light/dark setting. com.example.base.ui.ThemeToggle lets a user override it.
|
||||||
|
@ColorScheme(ColorScheme.Value.SYSTEM)
|
||||||
@Push
|
@Push
|
||||||
public class Application implements AppShellConfigurator {
|
public class Application implements AppShellConfigurator {
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ public final class MainLayout extends AppLayout {
|
|||||||
|
|
||||||
// AuthenticationContext is not Serializable by design, so this field must stay transient.
|
// AuthenticationContext is not Serializable by design, so this field must stay transient.
|
||||||
private final transient AuthenticationContext authenticationContext;
|
private final transient AuthenticationContext authenticationContext;
|
||||||
|
private final ThemePreference themePreference;
|
||||||
|
|
||||||
MainLayout(AuthenticationContext authenticationContext) {
|
MainLayout(AuthenticationContext authenticationContext, ThemePreference themePreference) {
|
||||||
this.authenticationContext = authenticationContext;
|
this.authenticationContext = authenticationContext;
|
||||||
|
this.themePreference = themePreference;
|
||||||
setPrimarySection(Section.DRAWER);
|
setPrimarySection(Section.DRAWER);
|
||||||
addToDrawer(createApplicationHeader(), createApplicationDrawer(), createApplicationFooter());
|
addToDrawer(createApplicationHeader(), createApplicationDrawer(), createApplicationFooter());
|
||||||
}
|
}
|
||||||
@@ -40,8 +42,13 @@ public final class MainLayout extends AppLayout {
|
|||||||
var appName = new Span("My Application");
|
var appName = new Span("My Application");
|
||||||
appName.addClassName("app-name");
|
appName.addClassName("app-name");
|
||||||
|
|
||||||
var header = new HorizontalLayout(appLogo, appName);
|
var nameGroup = new HorizontalLayout(appLogo, appName);
|
||||||
|
nameGroup.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||||
|
|
||||||
|
var header = new HorizontalLayout(nameGroup, new ThemeToggle(themePreference));
|
||||||
header.setAlignItems(FlexComponent.Alignment.CENTER);
|
header.setAlignItems(FlexComponent.Alignment.CENTER);
|
||||||
|
header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN);
|
||||||
|
header.setWidthFull();
|
||||||
header.setPadding(true);
|
header.setPadding(true);
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.example.base.ui;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.page.ColorScheme;
|
||||||
|
import com.vaadin.flow.server.VaadinRequest;
|
||||||
|
import com.vaadin.flow.server.VaadinResponse;
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads and writes the visitor's chosen {@link ColorScheme.Value} as a cookie, so it survives
|
||||||
|
* page reloads and future sessions without needing any client-side script. Kept as a plain
|
||||||
|
* utility class (rather than folded into {@link ThemePreference}) so both {@link ThemeInitializer}
|
||||||
|
* (which only ever has a raw request/response, not a session-scoped bean) and
|
||||||
|
* {@link ThemePreference} can share the exact same encoding.
|
||||||
|
*/
|
||||||
|
final class ThemeCookie {
|
||||||
|
|
||||||
|
static final String NAME = "app-theme";
|
||||||
|
|
||||||
|
private static final int MAX_AGE_SECONDS = (int) Duration.ofDays(365).toSeconds();
|
||||||
|
|
||||||
|
private ThemeCookie() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the stored preference, if any cookie was sent with the request. */
|
||||||
|
static Optional<ColorScheme.Value> read(@Nullable VaadinRequest request) {
|
||||||
|
if (request == null || request.getCookies() == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Arrays.stream(request.getCookies())
|
||||||
|
.filter(cookie -> NAME.equals(cookie.getName()))
|
||||||
|
.map(Cookie::getValue)
|
||||||
|
.map(ColorScheme.Value::fromString)
|
||||||
|
.filter(value -> value != ColorScheme.Value.NORMAL)
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Persists the given preference for a year, scoped to the whole application. */
|
||||||
|
static void write(@Nullable VaadinResponse response, ColorScheme.Value value) {
|
||||||
|
if (response == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var cookie = new Cookie(NAME, value.getValue());
|
||||||
|
cookie.setPath("/");
|
||||||
|
cookie.setMaxAge(MAX_AGE_SECONDS);
|
||||||
|
cookie.setHttpOnly(true);
|
||||||
|
response.addCookie(cookie);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.example.base.ui;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.page.ColorScheme;
|
||||||
|
import com.vaadin.flow.server.ServiceInitEvent;
|
||||||
|
import com.vaadin.flow.server.VaadinRequest;
|
||||||
|
import com.vaadin.flow.server.VaadinServiceInitListener;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores a visitor's stored light/dark preference (see {@link ThemeCookie}) on every visit.
|
||||||
|
* Two things happen, mirroring what {@code @ColorScheme} plus {@code Page#setColorScheme} do
|
||||||
|
* internally (see {@code IndexHtmlRequestHandler#applyColorScheme} / {@code Page#setColorScheme}
|
||||||
|
* in flow-server 25.2.5), so a returning visitor never sees a flash of the wrong theme:
|
||||||
|
* <ul>
|
||||||
|
* <li>Before the page is sent, the {@code <html>} element's {@code theme} attribute and
|
||||||
|
* inline {@code color-scheme} style are overwritten from the cookie, taking precedence over
|
||||||
|
* the {@code @ColorScheme(SYSTEM)} default declared on {@code Application}.</li>
|
||||||
|
* <li>On UI init, the same cookie seeds the session's {@link ThemePreference} bean, so
|
||||||
|
* {@link ThemeToggle} renders the right icon/label immediately, with no client round-trip.</li>
|
||||||
|
* </ul>
|
||||||
|
* First-ever visits (no cookie yet) are left alone and simply follow {@code @ColorScheme(SYSTEM)}.
|
||||||
|
* <p>
|
||||||
|
* Wired the same way as {@code com.example.security.TwoFactorNavigationGuard}: a
|
||||||
|
* {@code VaadinServiceInitListener} bean registering a listener in {@code serviceInit}.
|
||||||
|
* <p>
|
||||||
|
* {@link ThemePreference} is looked up lazily through an {@link ObjectProvider} rather than
|
||||||
|
* injected directly: this class is an application-wide singleton created at startup, well before
|
||||||
|
* any Vaadin session exists, whereas {@code ThemePreference} only resolves inside one - the
|
||||||
|
* lookup has to happen inside the UI-init callback, once a session is actually active.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
class ThemeInitializer implements VaadinServiceInitListener {
|
||||||
|
|
||||||
|
private final ObjectProvider<ThemePreference> themePreference;
|
||||||
|
|
||||||
|
ThemeInitializer(ObjectProvider<ThemePreference> themePreference) {
|
||||||
|
this.themePreference = themePreference;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void serviceInit(ServiceInitEvent event) {
|
||||||
|
event.addIndexHtmlRequestListener(response -> ThemeCookie.read(response.getVaadinRequest())
|
||||||
|
.ifPresent(value -> applyToDocumentRoot(response.getDocument().head().parent(), value)));
|
||||||
|
|
||||||
|
event.getSource().addUIInitListener(uiEvent -> ThemeCookie.read(VaadinRequest.getCurrent())
|
||||||
|
.ifPresent(value -> themePreference.getObject().seed(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyToDocumentRoot(Element html, ColorScheme.Value value) {
|
||||||
|
if (html == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
html.attr("theme", value.getThemeValue());
|
||||||
|
html.attr("style", "color-scheme: " + value.getValue() + ";");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.example.base.ui;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.UI;
|
||||||
|
import com.vaadin.flow.component.page.ColorScheme;
|
||||||
|
import com.vaadin.flow.server.VaadinService;
|
||||||
|
import com.vaadin.flow.spring.annotation.VaadinSessionScope;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the current visitor's light/dark preference for the session and applies changes to it.
|
||||||
|
* Same shape as {@code com.example.security.TwoFactorAttemptTracker}: a session-scoped bean
|
||||||
|
* holding small mutable UI state. Public: injected into {@link MainLayout} and {@link ThemeToggle}.
|
||||||
|
* <p>
|
||||||
|
* The preference is also written to a cookie (see {@link ThemeCookie}) so it survives beyond the
|
||||||
|
* session too. {@link ThemeInitializer} reads that cookie back on the next visit, both to render
|
||||||
|
* the correct theme before first paint and to seed this bean for the new session.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@VaadinSessionScope
|
||||||
|
public class ThemePreference {
|
||||||
|
|
||||||
|
private ColorScheme.Value current = ColorScheme.Value.SYSTEM;
|
||||||
|
|
||||||
|
public ColorScheme.Value get() {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True once the visitor is explicitly in dark mode (as opposed to SYSTEM/LIGHT). */
|
||||||
|
public boolean isDark() {
|
||||||
|
return current == ColorScheme.Value.DARK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Flips between light and dark; an undecided SYSTEM preference is treated as light. */
|
||||||
|
public void toggle() {
|
||||||
|
apply(isDark() ? ColorScheme.Value.LIGHT : ColorScheme.Value.DARK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Used by {@link ThemeInitializer} to seed this bean from the stored cookie, without re-applying it. */
|
||||||
|
void seed(ColorScheme.Value value) {
|
||||||
|
current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void apply(ColorScheme.Value value) {
|
||||||
|
current = value;
|
||||||
|
UI.getCurrent().getPage().setColorScheme(value);
|
||||||
|
ThemeCookie.write(VaadinService.getCurrentResponse(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.example.base.ui;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.Composite;
|
||||||
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
|
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An icon-only button in the application header that flips the app between light and dark mode.
|
||||||
|
* Built the same way as {@link OtpField} / {@link ViewTitle}: a small server-side Java composite.
|
||||||
|
* <p>
|
||||||
|
* There's no dedicated toggle/switch component in Vaadin 25 (Aura has no {@code ToggleButton}),
|
||||||
|
* so a tertiary icon {@link Button} is the idiomatic stand-in for a header action like this.
|
||||||
|
*/
|
||||||
|
class ThemeToggle extends Composite<Button> {
|
||||||
|
|
||||||
|
// Package-private (rather than private) so ThemeToggleTest can click it directly.
|
||||||
|
final Button button;
|
||||||
|
|
||||||
|
private final ThemePreference themePreference;
|
||||||
|
|
||||||
|
ThemeToggle(ThemePreference themePreference) {
|
||||||
|
this.themePreference = themePreference;
|
||||||
|
|
||||||
|
button = getContent();
|
||||||
|
button.addClassName("theme-toggle");
|
||||||
|
button.addThemeVariants(ButtonVariant.TERTIARY);
|
||||||
|
button.addClickListener(event -> {
|
||||||
|
themePreference.toggle();
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void render() {
|
||||||
|
var dark = themePreference.isDark();
|
||||||
|
button.setIcon((dark ? VaadinIcon.SUN_O : VaadinIcon.MOON_O).create());
|
||||||
|
var label = dark ? "Switch to light theme" : "Switch to dark theme";
|
||||||
|
button.setAriaLabel(label);
|
||||||
|
button.setTooltipText(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Solstice - a custom Aura theme variant.
|
||||||
|
*
|
||||||
|
* Concept: the two color schemes are the two solstices. Light mode is warm summer daylight
|
||||||
|
* (amber accent, warm paper background); dark mode is cool winter midnight (aurora cyan accent,
|
||||||
|
* deep blue background). Aura resolves each `--aura-*-light`/`--aura-*-dark` pair through the
|
||||||
|
* native CSS `light-dark()` function based on the `color-scheme` set on <html> - see
|
||||||
|
* com.example.base.ui.ThemePreference / ThemeInitializer for how that gets set.
|
||||||
|
*
|
||||||
|
* Only *-light/-dark suffixed custom properties are overridden here; everything else (components,
|
||||||
|
* other CSS in this app) should keep consuming the unsuffixed tokens (--aura-accent-color,
|
||||||
|
* --vaadin-text-color, etc.) so it automatically follows whichever scheme is active.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Accent: burnt amber by day, aurora cyan by night. */
|
||||||
|
--aura-accent-color-light: oklch(0.58 0.16 55);
|
||||||
|
--aura-accent-color-dark: oklch(0.80 0.12 195);
|
||||||
|
|
||||||
|
/* App background: warm paper by day, deep midnight blue by night. */
|
||||||
|
--aura-background-color-light: oklch(0.97 0.018 80);
|
||||||
|
--aura-background-color-dark: oklch(0.19 0.03 255);
|
||||||
|
|
||||||
|
/* A touch softer and slightly more contrast than the Aura defaults. */
|
||||||
|
--aura-base-radius: 6;
|
||||||
|
--aura-contrast-level: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo and app name pick up the accent so the "identity" of the theme is visible immediately. */
|
||||||
|
.app-logo {
|
||||||
|
--vaadin-avatar-background: var(--aura-accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
background: linear-gradient(120deg, var(--aura-accent-color), var(--aura-accent-color-dark, var(--aura-accent-color)));
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The toggle gets a small icon transition so flipping it feels intentional rather than instant. */
|
||||||
|
.theme-toggle vaadin-icon {
|
||||||
|
transition: transform 0.25s ease, color 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover vaadin-icon {
|
||||||
|
transform: rotate(-14deg) scale(1.1);
|
||||||
|
color: var(--aura-accent-color);
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.example.base.ui;
|
||||||
|
|
||||||
|
import com.vaadin.browserless.SpringBrowserlessTest;
|
||||||
|
import com.vaadin.flow.component.icon.Icon;
|
||||||
|
import com.vaadin.flow.component.page.ColorScheme;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.security.test.context.support.WithMockUser;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
|
||||||
|
@Transactional
|
||||||
|
@WithMockUser(roles = "USER")
|
||||||
|
class ThemeToggleTest extends SpringBrowserlessTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void default_state_offers_to_switch_to_dark_theme() {
|
||||||
|
var view = navigate(ThemeToggleTestView.class);
|
||||||
|
|
||||||
|
assertThat(view.themePreference.get()).isEqualTo(ColorScheme.Value.SYSTEM);
|
||||||
|
assertThat(view.themeToggle.button.getAriaLabel()).contains("Switch to dark theme");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clicking_switches_to_dark_theme() {
|
||||||
|
var view = navigate(ThemeToggleTestView.class);
|
||||||
|
|
||||||
|
test(view.themeToggle.button).click();
|
||||||
|
|
||||||
|
assertThat(view.themePreference.get()).isEqualTo(ColorScheme.Value.DARK);
|
||||||
|
assertThat(view.themePreference.isDark()).isTrue();
|
||||||
|
assertThat(view.themeToggle.button.getAriaLabel()).contains("Switch to light theme");
|
||||||
|
assertThat(((Icon) view.themeToggle.button.getIcon()).getIcon()).isEqualTo("vaadin:sun-o");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clicking_twice_returns_to_light_theme() {
|
||||||
|
var view = navigate(ThemeToggleTestView.class);
|
||||||
|
|
||||||
|
test(view.themeToggle.button).click();
|
||||||
|
test(view.themeToggle.button).click();
|
||||||
|
|
||||||
|
assertThat(view.themePreference.get()).isEqualTo(ColorScheme.Value.LIGHT);
|
||||||
|
assertThat(view.themePreference.isDark()).isFalse();
|
||||||
|
assertThat(view.themeToggle.button.getAriaLabel()).contains("Switch to dark theme");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.example.base.ui;
|
||||||
|
|
||||||
|
import com.vaadin.flow.component.html.Div;
|
||||||
|
import com.vaadin.flow.router.PageTitle;
|
||||||
|
import com.vaadin.flow.router.Route;
|
||||||
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
|
||||||
|
/** Test-only host route so ThemeToggleTest can exercise the component inside an attached UI. */
|
||||||
|
@Route("test/theme-toggle")
|
||||||
|
@PageTitle("ThemeToggle test host")
|
||||||
|
@PermitAll
|
||||||
|
class ThemeToggleTestView extends Div {
|
||||||
|
|
||||||
|
final ThemeToggle themeToggle;
|
||||||
|
final ThemePreference themePreference;
|
||||||
|
|
||||||
|
ThemeToggleTestView(ThemePreference themePreference) {
|
||||||
|
this.themePreference = themePreference;
|
||||||
|
themeToggle = new ThemeToggle(themePreference);
|
||||||
|
add(themeToggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user