[FEATURE] Dark Theme
This commit is contained in:
+3
-21
@@ -1,30 +1,12 @@
|
|||||||
FROM ghcr.io/jqlang/jq:latest AS jq-stage
|
FROM eclipse-temurin:25-jdk AS build
|
||||||
|
|
||||||
FROM eclipse-temurin:21-jdk AS build
|
|
||||||
COPY --from=jq-stage /jq /usr/bin/jq
|
|
||||||
# Test that jq works after copying
|
|
||||||
RUN jq --version
|
|
||||||
|
|
||||||
ENV HOME=/app
|
ENV HOME=/app
|
||||||
RUN mkdir -p $HOME
|
RUN mkdir -p $HOME
|
||||||
WORKDIR $HOME
|
WORKDIR $HOME
|
||||||
COPY . $HOME
|
COPY . $HOME
|
||||||
|
|
||||||
# If you have a Vaadin Pro key, pass it as a secret with id "proKey":
|
|
||||||
#
|
|
||||||
# $ docker build --secret id=proKey,src=$HOME/.vaadin/proKey .
|
|
||||||
#
|
|
||||||
# If you have a Vaadin Offline key, pass it as a secret with id "offlineKey":
|
|
||||||
#
|
|
||||||
# $ docker build --secret id=offlineKey,src=$HOME/.vaadin/offlineKey .
|
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.m2 \
|
RUN --mount=type=cache,target=/root/.m2 \
|
||||||
--mount=type=secret,id=proKey \
|
./mvnw clean package -DskipTests
|
||||||
--mount=type=secret,id=offlineKey \
|
|
||||||
sh -c 'PRO_KEY=$(jq -r ".proKey // empty" /run/secrets/proKey 2>/dev/null || echo "") && \
|
|
||||||
OFFLINE_KEY=$(cat /run/secrets/offlineKey 2>/dev/null || echo "") && \
|
|
||||||
./mvnw clean package -DskipTests -Dvaadin.proKey=${PRO_KEY} -Dvaadin.offlineKey=${OFFLINE_KEY}'
|
|
||||||
|
|
||||||
FROM eclipse-temurin:21-jre-alpine
|
FROM eclipse-temurin:25-jre-alpine
|
||||||
COPY --from=build /app/target/*.jar app.jar
|
COPY --from=build /app/target/*.jar app.jar
|
||||||
ENTRYPOINT ["java", "-jar", "/app.jar", "--spring.profiles.active=prod"]
|
ENTRYPOINT ["java", "-jar", "/app.jar", "--spring.profiles.active=prod"]
|
||||||
|
|||||||
Binary file not shown.
@@ -2,14 +2,78 @@ import { html, LitElement } from 'lit';
|
|||||||
import { customElement } from 'lit/decorators.js';
|
import { customElement } from 'lit/decorators.js';
|
||||||
import ApexCharts from 'apexcharts';
|
import ApexCharts from 'apexcharts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a CSS custom property (which may be a `light-dark()` value) to its
|
||||||
|
* currently active value for the given scheme. Reading the custom property
|
||||||
|
* directly (e.g. via getComputedStyle on an element that just has the var set)
|
||||||
|
* returns the raw `light-dark(...)` text, not the resolved color — resolution
|
||||||
|
* only happens when the property is used as an actual style value, hence the
|
||||||
|
* throwaway probe element.
|
||||||
|
*/
|
||||||
|
function resolveColor(varName: string): string {
|
||||||
|
const probe = document.createElement('span');
|
||||||
|
probe.style.color = `var(${varName})`;
|
||||||
|
probe.style.display = 'none';
|
||||||
|
document.documentElement.appendChild(probe);
|
||||||
|
const resolved = getComputedStyle(probe).color;
|
||||||
|
probe.remove();
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDarkScheme(): boolean {
|
||||||
|
return getComputedStyle(document.documentElement).colorScheme.includes('dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Overlays theme-varying option keys with values resolved from the current
|
||||||
|
* --dialect-* CSS tokens, so charts follow the light/dark toggle without a
|
||||||
|
* server round-trip. */
|
||||||
|
function applyThemeOverlay(options: any): any {
|
||||||
|
const borderColor = resolveColor('--dialect-border');
|
||||||
|
const inkColor = resolveColor('--dialect-ink');
|
||||||
|
const dark = isDarkScheme();
|
||||||
|
|
||||||
|
options.grid = { ...options.grid, borderColor };
|
||||||
|
options.legend = {
|
||||||
|
...options.legend,
|
||||||
|
labels: { ...options.legend?.labels, colors: inkColor },
|
||||||
|
};
|
||||||
|
options.tooltip = { ...options.tooltip, theme: dark ? 'dark' : 'light' };
|
||||||
|
|
||||||
|
if (options.xaxis) {
|
||||||
|
options.xaxis = {
|
||||||
|
...options.xaxis,
|
||||||
|
labels: { ...options.xaxis.labels, style: { ...options.xaxis.labels?.style, colors: inkColor } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (options.yaxis) {
|
||||||
|
options.yaxis = {
|
||||||
|
...options.yaxis,
|
||||||
|
labels: { ...options.yaxis.labels, style: { ...options.yaxis.labels?.style, colors: inkColor } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
@customElement('apex-chart')
|
@customElement('apex-chart')
|
||||||
export class ApexChart extends LitElement {
|
export class ApexChart extends LitElement {
|
||||||
private chart?: ApexCharts;
|
private chart?: ApexCharts;
|
||||||
|
private lastOptions?: any;
|
||||||
|
|
||||||
|
private readonly onThemeChange = () => {
|
||||||
|
if (!this.chart || !this.lastOptions) return;
|
||||||
|
this.chart.updateOptions(applyThemeOverlay(this.lastOptions));
|
||||||
|
};
|
||||||
|
|
||||||
protected createRenderRoot() {
|
protected createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
window.addEventListener('dialect-theme-change', this.onThemeChange);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`<div id="container" style="width:100%;height:100%"></div>`;
|
return html`<div id="container" style="width:100%;height:100%"></div>`;
|
||||||
}
|
}
|
||||||
@@ -27,17 +91,21 @@ export class ApexChart extends LitElement {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.lastOptions = options;
|
||||||
|
const themed = applyThemeOverlay(options);
|
||||||
|
|
||||||
const container = this.querySelector('#container') as HTMLElement;
|
const container = this.querySelector('#container') as HTMLElement;
|
||||||
if (this.chart) {
|
if (this.chart) {
|
||||||
await this.chart.updateOptions(options);
|
await this.chart.updateOptions(themed);
|
||||||
} else {
|
} else {
|
||||||
this.chart = new ApexCharts(container, options);
|
this.chart = new ApexCharts(container, themed);
|
||||||
await this.chart.render();
|
await this.chart.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
|
window.removeEventListener('dialect-theme-change', this.onThemeChange);
|
||||||
this.chart?.destroy();
|
this.chart?.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,16 +24,6 @@ public final class DialectTheme {
|
|||||||
private static final String FONT_FAMILY =
|
private static final String FONT_FAMILY =
|
||||||
"'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
"'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() {
|
private DialectTheme() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,20 +40,13 @@ public final class DialectTheme {
|
|||||||
|
|
||||||
options.put("colors", COLORS);
|
options.put("colors", COLORS);
|
||||||
|
|
||||||
options.put("grid", Map.of(
|
// grid.borderColor, legend.labels.colors and tooltip.theme are theme-varying
|
||||||
"borderColor", BORDER_COLOR,
|
// (light vs. dark) and get overlaid client-side from the --dialect-* CSS
|
||||||
"strokeDashArray", 4
|
// tokens — see applyThemeOverlay() in apex-chart.ts.
|
||||||
));
|
options.put("grid", Map.of("strokeDashArray", 4));
|
||||||
|
options.put("legend", Map.of("position", "bottom"));
|
||||||
options.put("legend", Map.of(
|
|
||||||
"position", "bottom",
|
|
||||||
"labels", Map.of("colors", INK_COLOR)
|
|
||||||
));
|
|
||||||
|
|
||||||
options.put("dataLabels", Map.of("enabled", false));
|
options.put("dataLabels", Map.of("enabled", false));
|
||||||
|
|
||||||
options.put("tooltip", Map.of("theme", "light"));
|
|
||||||
|
|
||||||
if ("line".equals(chartType)) {
|
if ("line".equals(chartType)) {
|
||||||
options.put("stroke", Map.of("curve", "smooth", "width", 3));
|
options.put("stroke", Map.of("curve", "smooth", "width", 3));
|
||||||
} else if ("bar".equals(chartType)) {
|
} else if ("bar".equals(chartType)) {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.example.js;
|
||||||
|
|
||||||
|
public final class CommonJS {
|
||||||
|
|
||||||
|
private CommonJS() {}
|
||||||
|
|
||||||
|
/** Sets color-scheme on <html> from the saved choice, falling back to the OS
|
||||||
|
* preference (prefers-color-scheme) when no choice was saved, and
|
||||||
|
* returns "light"/"dark" so the button's initial icon can match. */
|
||||||
|
public static final String INIT_THEME_JS = """
|
||||||
|
const saved = localStorage.getItem('dialect-theme');
|
||||||
|
const theme = saved
|
||||||
|
|| (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||||
|
document.documentElement.style.colorScheme = theme;
|
||||||
|
return theme;
|
||||||
|
""";
|
||||||
|
|
||||||
|
public static final String TOGGLE_THEME_JS = """
|
||||||
|
const next = document.documentElement.style.colorScheme === 'dark' ? 'light' : 'dark';
|
||||||
|
document.documentElement.style.colorScheme = next;
|
||||||
|
localStorage.setItem('dialect-theme', next);
|
||||||
|
window.dispatchEvent(new CustomEvent('dialect-theme-change'));
|
||||||
|
return next;
|
||||||
|
""";
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.example.views;
|
package com.example.views;
|
||||||
|
|
||||||
|
import com.example.js.CommonJS;
|
||||||
import com.vaadin.flow.component.applayout.AppLayout;
|
import com.vaadin.flow.component.applayout.AppLayout;
|
||||||
import com.vaadin.flow.component.applayout.DrawerToggle;
|
import com.vaadin.flow.component.applayout.DrawerToggle;
|
||||||
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
import com.vaadin.flow.component.html.Span;
|
import com.vaadin.flow.component.html.Span;
|
||||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||||
import com.vaadin.flow.component.sidenav.SideNav;
|
import com.vaadin.flow.component.sidenav.SideNav;
|
||||||
@@ -11,13 +14,25 @@ import com.vaadin.flow.router.Layout;
|
|||||||
@Layout
|
@Layout
|
||||||
public class MainLayout extends AppLayout {
|
public class MainLayout extends AppLayout {
|
||||||
|
|
||||||
|
private final Button themeToggle = new Button(VaadinIcon.MOON.create());
|
||||||
|
|
||||||
public MainLayout() {
|
public MainLayout() {
|
||||||
DrawerToggle toggle = new DrawerToggle();
|
DrawerToggle toggle = new DrawerToggle();
|
||||||
|
|
||||||
Span title = new Span("Chart App");
|
Span title = new Span("Chart App");
|
||||||
title.addClassName("dialect-title");
|
title.addClassName("dialect-title");
|
||||||
|
|
||||||
addToNavbar(toggle, title);
|
Span spacer = new Span();
|
||||||
|
spacer.getStyle().set("flex-grow", "1");
|
||||||
|
|
||||||
|
themeToggle.addThemeVariants(ButtonVariant.TERTIARY, ButtonVariant.LUMO_ICON);
|
||||||
|
themeToggle.setAriaLabel("Farbschema wechseln");
|
||||||
|
themeToggle.addClickListener(e ->
|
||||||
|
getElement().executeJs(CommonJS.TOGGLE_THEME_JS).then(String.class, this::applyIconFor));
|
||||||
|
|
||||||
|
addToNavbar(toggle, title, spacer, themeToggle);
|
||||||
|
|
||||||
|
getElement().executeJs(CommonJS.INIT_THEME_JS).then(String.class, this::applyIconFor);
|
||||||
|
|
||||||
SideNav nav = new SideNav();
|
SideNav nav = new SideNav();
|
||||||
nav.addItem(new SideNavItem("Dashboard", DashboardView.class,
|
nav.addItem(new SideNavItem("Dashboard", DashboardView.class,
|
||||||
@@ -27,4 +42,8 @@ public class MainLayout extends AppLayout {
|
|||||||
|
|
||||||
addToDrawer(nav);
|
addToDrawer(nav);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyIconFor(String theme) {
|
||||||
|
themeToggle.setIcon("dark".equals(theme) ? VaadinIcon.SUN_O.create() : VaadinIcon.MOON.create());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
/* Dialect design system tokens (https://www.figma.com/design/6DRThj0vmRgfjMIdsmkaIy) */
|
/* Dialect design system tokens (https://www.figma.com/design/6DRThj0vmRgfjMIdsmkaIy) */
|
||||||
:root {
|
:root {
|
||||||
|
/* Support both schemes; actual scheme is picked by the theme toggle setting
|
||||||
|
`color-scheme` on <html> (see MainLayout's theme switch). */
|
||||||
|
color-scheme: light dark;
|
||||||
|
|
||||||
--dialect-primary: #e86c00;
|
--dialect-primary: #e86c00;
|
||||||
--dialect-secondary: #f6b93b;
|
--dialect-secondary: #f6b93b;
|
||||||
--dialect-bg: #f4f6f9;
|
--dialect-bg: light-dark(#f4f6f9, #14171f);
|
||||||
--dialect-surface: #ffffff;
|
--dialect-surface: light-dark(#ffffff, #1c2029);
|
||||||
--dialect-ink: #14171f;
|
--dialect-ink: light-dark(#14171f, #e6eaf0);
|
||||||
--dialect-border: #e6eaf0;
|
--dialect-border: light-dark(#e6eaf0, #2b303b);
|
||||||
--dialect-radius: 14px;
|
--dialect-radius: 14px;
|
||||||
--dialect-shadow: 0 6px 20px rgba(20, 23, 31, 0.08), 0 1px 2px rgba(20, 23, 31, 0.06);
|
/* light-dark() only takes 2 args, so each shadow layer gets its own light-dark()
|
||||||
|
call and the layers are joined below (var() is a text substitution, so the
|
||||||
|
comma-separated shadow list still parses correctly). */
|
||||||
|
--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));
|
||||||
|
--dialect-shadow: var(--dialect-shadow-1), var(--dialect-shadow-2);
|
||||||
|
--dialect-navbar-shadow: light-dark(0 1px 3px rgba(20, 23, 31, 0.05), 0 1px 3px rgba(0, 0, 0, 0.3));
|
||||||
|
|
||||||
/* Alias onto Aura's theming hooks */
|
/* Alias onto Aura's theming hooks */
|
||||||
--aura-accent-color-light: var(--dialect-primary);
|
--aura-accent-color-light: var(--dialect-primary);
|
||||||
--aura-accent-color-dark: var(--dialect-primary);
|
--aura-accent-color-dark: var(--dialect-primary);
|
||||||
--aura-background-color-light: var(--dialect-bg);
|
--aura-background-color-light: var(--dialect-bg);
|
||||||
|
--aura-background-color-dark: var(--dialect-bg);
|
||||||
--aura-orange: var(--dialect-primary);
|
--aura-orange: var(--dialect-primary);
|
||||||
--aura-yellow: var(--dialect-secondary);
|
--aura-yellow: var(--dialect-secondary);
|
||||||
|
|
||||||
@@ -59,7 +70,7 @@ vaadin-app-layout::part(navbar) {
|
|||||||
-webkit-backdrop-filter: none;
|
-webkit-backdrop-filter: none;
|
||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
border-bottom: 1px solid var(--dialect-border);
|
border-bottom: 1px solid var(--dialect-border);
|
||||||
box-shadow: 0 1px 3px rgba(20, 23, 31, 0.05);
|
box-shadow: var(--dialect-navbar-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar: solid white, right divider separates it from the canvas */
|
/* Sidebar: solid white, right divider separates it from the canvas */
|
||||||
|
|||||||
Reference in New Issue
Block a user