From 3078a933d62740dc7fcfa643eeeae7d51471137b Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Fri, 28 Nov 2025 14:03:41 +0400 Subject: [PATCH 1/3] feat: example theme switcher --- dspublisher/config/default.json | 4 ++- frontend/demo/foundation/lumo-tokens.ts | 2 +- frontend/demo/init.ts | 17 +++++----- frontend/demo/theme.ts | 44 ++++++++++++++++++++----- frontend/themes/docs/styles.css | 4 +-- 5 files changed, 51 insertions(+), 20 deletions(-) diff --git a/dspublisher/config/default.json b/dspublisher/config/default.json index 22035408a6..d4397a681f 100644 --- a/dspublisher/config/default.json +++ b/dspublisher/config/default.json @@ -8,5 +8,7 @@ "Flow", "React", "Lit" - ] + ], + "exampleThemes": ["Lumo", "Aura"], + "defaultExampleTheme": "Lumo" } diff --git a/frontend/demo/foundation/lumo-tokens.ts b/frontend/demo/foundation/lumo-tokens.ts index 946e96d2b9..faf08cb4e2 100644 --- a/frontend/demo/foundation/lumo-tokens.ts +++ b/frontend/demo/foundation/lumo-tokens.ts @@ -1,5 +1,5 @@ -import propsStyles from '@vaadin/vaadin-lumo-styles/src/props/index.css?inline'; import colorScheme from '@vaadin/vaadin-lumo-styles/src/global/color-scheme.css?inline'; +import propsStyles from '@vaadin/vaadin-lumo-styles/src/props/index.css?inline'; import utilityStyles from '@vaadin/vaadin-lumo-styles/utility.css?inline'; import { addStylesheet } from 'Frontend/demo/theme'; diff --git a/frontend/demo/init.ts b/frontend/demo/init.ts index dd05111e0d..b3e1cbfba0 100644 --- a/frontend/demo/init.ts +++ b/frontend/demo/init.ts @@ -6,22 +6,23 @@ import '../generated/vaadin-featureflags'; import { applyTheme } from 'Frontend/demo/theme'; import client from 'Frontend/generated/connect-client.default'; -// Apply the theme to the document, so that notification styles work as expected, -// as the notification container is added to the document body. -applyTheme(document); +// Apply the theme to the document when example is rendered as a standalone page +const searchParams = new URLSearchParams(window.location.search); +if (searchParams.has('example-theme')) { + applyTheme(document); +} // @ts-expect-error Inserted by DS Publisher client.prefix = __VAADIN_CONNECT_PREFIX__; // eslint-disable-line no-undef -document.body.style.setProperty('--docs-example-render-font-family', 'var(--lumo-font-family)'); -document.body.style.setProperty('--docs-example-render-color', 'var(--lumo-body-text-color)'); -document.body.style.setProperty('--docs-example-render-background-color', 'var(--lumo-base-color)'); +// document.body.style.setProperty('--docs-example-render-font-family', 'var(--lumo-font-family)'); +// document.body.style.setProperty('--docs-example-render-color', 'var(--lumo-body-text-color)'); +// document.body.style.setProperty('--docs-example-render-background-color', 'var(--lumo-base-color)'); // Ensures standalone UI sample pags have a lang attribute document.documentElement.setAttribute('lang', 'en'); // Applies input field borders based on a `borders` URL parameter -const url = new URL(window.location.href); -if (url.searchParams.has('borders')) { +if (searchParams.has('borders')) { document.body.style.setProperty('--vaadin-input-field-border-width', '1px'); } diff --git a/frontend/demo/theme.ts b/frontend/demo/theme.ts index a9ae5a5223..75fec28ad0 100644 --- a/frontend/demo/theme.ts +++ b/frontend/demo/theme.ts @@ -1,4 +1,6 @@ import type { CSSResultGroup } from 'lit'; +import auraCss from '@vaadin/aura/aura.css?inline'; +import lumoCss from '@vaadin/vaadin-lumo-styles/lumo.css?inline'; import docsCss from 'Frontend/themes/docs/styles.css?inline'; function createStylesheet(css: CSSResultGroup | string): CSSStyleSheet { @@ -6,19 +8,45 @@ function createStylesheet(css: CSSResultGroup | string): CSSStyleSheet { // CSS imported with ?inline is actually a string in DSP, but typed as CSSResultGroup due to TS // types generated by Flow. // eslint-disable-next-line @typescript-eslint/no-base-to-string - const cssString = css.toString(); - stylesheet.replaceSync(cssString); + stylesheet.replaceSync(css.toString()); return stylesheet; } -const docsStylesheet = createStylesheet(docsCss); +const lumoStyleSheet = createStylesheet(lumoCss); +const auraStyleSheet = createStylesheet(auraCss); +const docsStyleSheet = createStylesheet(docsCss); + +function getRootHost(root: DocumentOrShadowRoot) { + let host: Element; + if (root instanceof ShadowRoot) { + host = root.host; + } else { + host = (root as Document).documentElement; + } + return host; +} + +function setExampleTheme(root: DocumentOrShadowRoot, exampleTheme: string | null) { + const adoptedStyleSheets = root.adoptedStyleSheets.filter( + (styleSheet) => ![lumoStyleSheet, auraStyleSheet, docsStyleSheet].includes(styleSheet) + ); + if (exampleTheme === 'Aura') { + adoptedStyleSheets.push(auraStyleSheet); + } + if (exampleTheme === 'Lumo') { + adoptedStyleSheets.push(lumoStyleSheet); + } + root.adoptedStyleSheets = [...adoptedStyleSheets, docsStyleSheet]; +} export function applyTheme(root: DocumentFragment | DocumentOrShadowRoot | HTMLElement) { - // The root parameter type is very broad to handle the default return type of - // LitElement.createRenderRoot. In general, we expect this to either be a document or a shadow - // root. The adoptedStyleSheets check below makes Typescript accept the parameter type. - if ('adoptedStyleSheets' in root && !root.adoptedStyleSheets.includes(docsStylesheet)) { - root.adoptedStyleSheets = [...root.adoptedStyleSheets, docsStylesheet]; + if (root instanceof ShadowRoot || root instanceof Document) { + const host = getRootHost(root); + setExampleTheme(root, host.getAttribute('example-theme')); + + new MutationObserver(() => { + setExampleTheme(root, host.getAttribute('example-theme')); + }).observe(host, { attributes: true, attributeFilter: ['example-theme'] }); } } diff --git a/frontend/themes/docs/styles.css b/frontend/themes/docs/styles.css index f0ba261f5d..b9877af766 100644 --- a/frontend/themes/docs/styles.css +++ b/frontend/themes/docs/styles.css @@ -1,6 +1,6 @@ /* Import Lumo theme and utility classes */ -@import '@vaadin/vaadin-lumo-styles/lumo.css'; -@import '@vaadin/vaadin-lumo-styles/utility.css'; +/* @import '@vaadin/vaadin-lumo-styles/lumo.css'; +@import '@vaadin/vaadin-lumo-styles/utility.css'; */ /* Import custom styles used in examples */ @import './app-layout.css'; From 4178c8bf981c40ea54c57b1bbf3159f98348c589 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Mon, 1 Dec 2025 18:00:39 +0400 Subject: [PATCH 2/3] clean up --- dspublisher/config/default.json | 4 +--- dspublisher/theme/global.css | 7 ++++++ frontend/demo/init.ts | 16 ++++++------- frontend/demo/theme.ts | 41 ++++++++++++++++++--------------- frontend/themes/docs/styles.css | 4 ---- vite.config.ts | 8 ++++++- 6 files changed, 46 insertions(+), 34 deletions(-) diff --git a/dspublisher/config/default.json b/dspublisher/config/default.json index d4397a681f..22035408a6 100644 --- a/dspublisher/config/default.json +++ b/dspublisher/config/default.json @@ -8,7 +8,5 @@ "Flow", "React", "Lit" - ], - "exampleThemes": ["Lumo", "Aura"], - "defaultExampleTheme": "Lumo" + ] } diff --git a/dspublisher/theme/global.css b/dspublisher/theme/global.css index b413c75765..bad5995139 100644 --- a/dspublisher/theme/global.css +++ b/dspublisher/theme/global.css @@ -90,6 +90,13 @@ html { --docs-before-background-color: var(--red-50); --docs-after-background-color: var(--green-100); --docs-before-after-border-color: var(--gray-300); + + /* Example frame styles currently use hardcoded values from the Lumo theme */ + --docs-example-render-font-family: + -apple-system, BlinkMacSystemFont, 'Roboto', 'Segoe UI', Helvetica, Arial, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + --docs-example-render-color: light-dark(hsla(214, 40%, 16%, 0.94), hsla(214, 96%, 96%, 0.9)); + --docs-example-render-background-color: light-dark(#fff, hsl(214, 35%, 21%)); } ::-moz-selection { diff --git a/frontend/demo/init.ts b/frontend/demo/init.ts index b3e1cbfba0..102d0e4c23 100644 --- a/frontend/demo/init.ts +++ b/frontend/demo/init.ts @@ -6,23 +6,23 @@ import '../generated/vaadin-featureflags'; import { applyTheme } from 'Frontend/demo/theme'; import client from 'Frontend/generated/connect-client.default'; -// Apply the theme to the document when example is rendered as a standalone page -const searchParams = new URLSearchParams(window.location.search); -if (searchParams.has('example-theme')) { +// Some Vaadin components add elements to document.body that require theme styles +// (e.g. Dialog). Such components are embedded via iframes, but the same examples +// can also be opened as standalone pages. To support both cases, apply the theme +// to the document when the example runs in an iframe or standalone. This is safe +// because in those modes the styles are isolated from the rest of the site. +if (window.location.pathname.endsWith('/example')) { applyTheme(document); } // @ts-expect-error Inserted by DS Publisher client.prefix = __VAADIN_CONNECT_PREFIX__; // eslint-disable-line no-undef -// document.body.style.setProperty('--docs-example-render-font-family', 'var(--lumo-font-family)'); -// document.body.style.setProperty('--docs-example-render-color', 'var(--lumo-body-text-color)'); -// document.body.style.setProperty('--docs-example-render-background-color', 'var(--lumo-base-color)'); - // Ensures standalone UI sample pags have a lang attribute document.documentElement.setAttribute('lang', 'en'); // Applies input field borders based on a `borders` URL parameter -if (searchParams.has('borders')) { +const url = new URL(window.location.href); +if (url.searchParams.has('borders')) { document.body.style.setProperty('--vaadin-input-field-border-width', '1px'); } diff --git a/frontend/demo/theme.ts b/frontend/demo/theme.ts index 75fec28ad0..475a6cb171 100644 --- a/frontend/demo/theme.ts +++ b/frontend/demo/theme.ts @@ -1,3 +1,4 @@ +import '@vaadin/vaadin-lumo-styles/src/props/icons.css'; import type { CSSResultGroup } from 'lit'; import auraCss from '@vaadin/aura/aura.css?inline'; import lumoCss from '@vaadin/vaadin-lumo-styles/lumo.css?inline'; @@ -8,7 +9,8 @@ function createStylesheet(css: CSSResultGroup | string): CSSStyleSheet { // CSS imported with ?inline is actually a string in DSP, but typed as CSSResultGroup due to TS // types generated by Flow. // eslint-disable-next-line @typescript-eslint/no-base-to-string - stylesheet.replaceSync(css.toString()); + const cssString = css.toString(); + stylesheet.replaceSync(cssString); return stylesheet; } @@ -26,27 +28,30 @@ function getRootHost(root: DocumentOrShadowRoot) { return host; } -function setExampleTheme(root: DocumentOrShadowRoot, exampleTheme: string | null) { - const adoptedStyleSheets = root.adoptedStyleSheets.filter( - (styleSheet) => ![lumoStyleSheet, auraStyleSheet, docsStyleSheet].includes(styleSheet) - ); - if (exampleTheme === 'Aura') { - adoptedStyleSheets.push(auraStyleSheet); - } - if (exampleTheme === 'Lumo') { - adoptedStyleSheets.push(lumoStyleSheet); - } - root.adoptedStyleSheets = [...adoptedStyleSheets, docsStyleSheet]; -} - export function applyTheme(root: DocumentFragment | DocumentOrShadowRoot | HTMLElement) { if (root instanceof ShadowRoot || root instanceof Document) { const host = getRootHost(root); - setExampleTheme(root, host.getAttribute('example-theme')); - new MutationObserver(() => { - setExampleTheme(root, host.getAttribute('example-theme')); - }).observe(host, { attributes: true, attributeFilter: ['example-theme'] }); + const updateTheme = () => { + const adoptedStyleSheets = root.adoptedStyleSheets.filter( + (styleSheet) => ![lumoStyleSheet, auraStyleSheet, docsStyleSheet].includes(styleSheet) + ); + + if (host.matches('[theme~="aura"]')) { + adoptedStyleSheets.push(auraStyleSheet); + } else { + adoptedStyleSheets.push(lumoStyleSheet); + } + + root.adoptedStyleSheets = [...adoptedStyleSheets, docsStyleSheet]; + }; + + new MutationObserver(updateTheme).observe(host, { + attributes: true, + attributeFilter: ['theme'], + }); + + updateTheme(); } } diff --git a/frontend/themes/docs/styles.css b/frontend/themes/docs/styles.css index b9877af766..1e3b5829e1 100644 --- a/frontend/themes/docs/styles.css +++ b/frontend/themes/docs/styles.css @@ -1,7 +1,3 @@ -/* Import Lumo theme and utility classes */ -/* @import '@vaadin/vaadin-lumo-styles/lumo.css'; -@import '@vaadin/vaadin-lumo-styles/utility.css'; */ - /* Import custom styles used in examples */ @import './app-layout.css'; @import './basic-layouts.css'; diff --git a/vite.config.ts b/vite.config.ts index f0d6566c7c..174139711c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -26,7 +26,7 @@ const customConfig: UserConfigFn = (env) => ({ }, { name: 'apply-docs-theme', - transform(_code, id) { + transform(code, id) { // This module is imported by web components exported from Flow to inject styles into their // shadow root. Instead of importing the docs styles from the Flow bundle again, for example // by adding @CssImport to DemoExporter, we provide a global from the docs @@ -36,6 +36,12 @@ const customConfig: UserConfigFn = (env) => ({ if (id.endsWith('generated/css.generated.js')) { return 'export const applyCss = window.__applyTheme.applyTheme;'; } + + // Remove applyCss(document) from `generated/vaadin-web-component.ts` because the global theme + // styles injection is handled by `init.ts` in the docs project. + if (id.endsWith('generated/vaadin-web-component.ts')) { + return code.replace('applyCss(document);', ''); + } }, }, ], From d21b20c91161584891cf2a5f1efd6a0526edd988 Mon Sep 17 00:00:00 2001 From: Sergey Vinogradov Date: Mon, 1 Dec 2025 19:40:47 +0400 Subject: [PATCH 3/3] Apply suggestion from @vursen --- frontend/demo/init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/demo/init.ts b/frontend/demo/init.ts index 102d0e4c23..7a53673e99 100644 --- a/frontend/demo/init.ts +++ b/frontend/demo/init.ts @@ -7,7 +7,7 @@ import { applyTheme } from 'Frontend/demo/theme'; import client from 'Frontend/generated/connect-client.default'; // Some Vaadin components add elements to document.body that require theme styles -// (e.g. Dialog). Such components are embedded via iframes, but the same examples +// (e.g. Notification). Such components are embedded via iframes, but the same examples // can also be opened as standalone pages. To support both cases, apply the theme // to the document when the example runs in an iframe or standalone. This is safe // because in those modes the styles are isolated from the rest of the site.