diff --git a/Dockerfile b/Dockerfile index 6b1484f..9b43d3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,30 +1,12 @@ -FROM ghcr.io/jqlang/jq:latest AS jq-stage - -FROM eclipse-temurin:21-jdk AS build -COPY --from=jq-stage /jq /usr/bin/jq -# Test that jq works after copying -RUN jq --version - +FROM eclipse-temurin:25-jdk AS build ENV HOME=/app RUN mkdir -p $HOME WORKDIR $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 \ - --mount=type=secret,id=proKey \ - --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}' + ./mvnw clean package -DskipTests -FROM eclipse-temurin:21-jre-alpine +FROM eclipse-temurin:25-jre-alpine COPY --from=build /app/target/*.jar app.jar ENTRYPOINT ["java", "-jar", "/app.jar", "--spring.profiles.active=prod"] diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle index f8124c7..ae23df9 100644 Binary files a/src/main/bundles/dev.bundle and b/src/main/bundles/dev.bundle differ diff --git a/src/main/frontend/components/apex-chart.ts b/src/main/frontend/components/apex-chart.ts index 4516c0d..dce4981 100644 --- a/src/main/frontend/components/apex-chart.ts +++ b/src/main/frontend/components/apex-chart.ts @@ -2,14 +2,78 @@ import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; 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') export class ApexChart extends LitElement { private chart?: ApexCharts; + private lastOptions?: any; + + private readonly onThemeChange = () => { + if (!this.chart || !this.lastOptions) return; + this.chart.updateOptions(applyThemeOverlay(this.lastOptions)); + }; protected createRenderRoot() { return this; } + connectedCallback() { + super.connectedCallback(); + window.addEventListener('dialect-theme-change', this.onThemeChange); + } + render() { return html`
`; } @@ -27,17 +91,21 @@ export class ApexChart extends LitElement { }, }; + this.lastOptions = options; + const themed = applyThemeOverlay(options); + const container = this.querySelector('#container') as HTMLElement; if (this.chart) { - await this.chart.updateOptions(options); + await this.chart.updateOptions(themed); } else { - this.chart = new ApexCharts(container, options); + this.chart = new ApexCharts(container, themed); await this.chart.render(); } } disconnectedCallback() { super.disconnectedCallback(); + window.removeEventListener('dialect-theme-change', this.onThemeChange); this.chart?.destroy(); } } \ No newline at end of file diff --git a/src/main/java/com/example/components/DialectTheme.java b/src/main/java/com/example/components/DialectTheme.java index 1eba763..cee25ad 100644 --- a/src/main/java/com/example/components/DialectTheme.java +++ b/src/main/java/com/example/components/DialectTheme.java @@ -24,16 +24,6 @@ public final class DialectTheme { 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() { } @@ -50,20 +40,13 @@ public final class DialectTheme { 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) - )); - + // grid.borderColor, legend.labels.colors and tooltip.theme are theme-varying + // (light vs. dark) and get overlaid client-side from the --dialect-* CSS + // tokens — see applyThemeOverlay() in apex-chart.ts. + options.put("grid", Map.of("strokeDashArray", 4)); + options.put("legend", Map.of("position", "bottom")); 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)) { diff --git a/src/main/java/com/example/js/CommonJS.java b/src/main/java/com/example/js/CommonJS.java new file mode 100644 index 0000000..5c82941 --- /dev/null +++ b/src/main/java/com/example/js/CommonJS.java @@ -0,0 +1,25 @@ +package com.example.js; + +public final class CommonJS { + + private CommonJS() {} + + /** Sets color-scheme on 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; + """; +} diff --git a/src/main/java/com/example/views/MainLayout.java b/src/main/java/com/example/views/MainLayout.java index 97f6d10..6c1e199 100644 --- a/src/main/java/com/example/views/MainLayout.java +++ b/src/main/java/com/example/views/MainLayout.java @@ -1,7 +1,10 @@ package com.example.views; +import com.example.js.CommonJS; import com.vaadin.flow.component.applayout.AppLayout; 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.icon.VaadinIcon; import com.vaadin.flow.component.sidenav.SideNav; @@ -11,13 +14,25 @@ import com.vaadin.flow.router.Layout; @Layout public class MainLayout extends AppLayout { + private final Button themeToggle = new Button(VaadinIcon.MOON.create()); + public MainLayout() { DrawerToggle toggle = new DrawerToggle(); Span title = new Span("Chart App"); 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(); nav.addItem(new SideNavItem("Dashboard", DashboardView.class, @@ -27,4 +42,8 @@ public class MainLayout extends AppLayout { addToDrawer(nav); } + + private void applyIconFor(String theme) { + themeToggle.setIcon("dark".equals(theme) ? VaadinIcon.SUN_O.create() : VaadinIcon.MOON.create()); + } } diff --git a/src/main/resources/META-INF/resources/styles.css b/src/main/resources/META-INF/resources/styles.css index 08f2217..eca9c26 100644 --- a/src/main/resources/META-INF/resources/styles.css +++ b/src/main/resources/META-INF/resources/styles.css @@ -1,18 +1,29 @@ /* Dialect design system tokens (https://www.figma.com/design/6DRThj0vmRgfjMIdsmkaIy) */ :root { + /* Support both schemes; actual scheme is picked by the theme toggle setting + `color-scheme` on (see MainLayout's theme switch). */ + color-scheme: light dark; + --dialect-primary: #e86c00; --dialect-secondary: #f6b93b; - --dialect-bg: #f4f6f9; - --dialect-surface: #ffffff; - --dialect-ink: #14171f; - --dialect-border: #e6eaf0; + --dialect-bg: light-dark(#f4f6f9, #14171f); + --dialect-surface: light-dark(#ffffff, #1c2029); + --dialect-ink: light-dark(#14171f, #e6eaf0); + --dialect-border: light-dark(#e6eaf0, #2b303b); --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 */ --aura-accent-color-light: var(--dialect-primary); --aura-accent-color-dark: var(--dialect-primary); --aura-background-color-light: var(--dialect-bg); + --aura-background-color-dark: var(--dialect-bg); --aura-orange: var(--dialect-primary); --aura-yellow: var(--dialect-secondary); @@ -59,7 +70,7 @@ vaadin-app-layout::part(navbar) { -webkit-backdrop-filter: none; backdrop-filter: none; 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 */