[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: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"]
|
||||
|
||||
Binary file not shown.
@@ -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`<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;
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <html> (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 */
|
||||
|
||||
Reference in New Issue
Block a user