From dde5a242b822ad98f3410f6c20cae62d413fcebc Mon Sep 17 00:00:00 2001 From: David Rios Date: Thu, 16 Jul 2026 11:49:09 -0400 Subject: [PATCH 1/6] fix(apollo-core): scope base design tokens to :root, .apollo-design for shadow DOM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generated variables.css holds the theme-invariant base tokens (raw color palette, font families, radius, …). Style Dictionary's built-in css/variables format scopes them to `:root {}` only, which never matches inside a shadow root (`:root` matches the document element; a shadow root is a DocumentFragment). Shadow-DOM canvas consumers (e.g. web-component embeds) therefore lose --font-mono/--font-title/--font-normal and the raw palette, falling back to Tailwind's generic ui-monospace stack. Add a css/variables-apollo-design format that delegates to the built-in formatter and rewrites only the selector to `:root, .apollo-design`, so the base tokens also apply inside a shadow root via the `apollo-design` theme wrapper Pattern-B consumers add — the same hook theme-variables.css already uses for semantic colors. Generated token body is byte-identical; only the selector changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/apollo-core/build-tokens.js | 19 +++++++++++++++++++ packages/apollo-core/src/tokens/config.js | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/apollo-core/build-tokens.js b/packages/apollo-core/build-tokens.js index e776c7b36..e7b65ef73 100644 --- a/packages/apollo-core/build-tokens.js +++ b/packages/apollo-core/build-tokens.js @@ -31,6 +31,25 @@ StyleDictionary.registerFormat({ formatter: _.template(fs.readFileSync(path.join(templateDir, 'css-theme-variables.template'))), }); +// The built-in `css/variables` format emits `variables.css`'s base tokens +// (palette, fonts, radius) under bare `:root {}`, which never matches inside a +// shadow root (`:root` = ; a shadow root is a DocumentFragment). +// +// So shadow-DOM canvas consumers lose the fonts and fall back to Tailwind's +// `ui-monospace`. Emitting `:root, .apollo-design` too makes them apply via the +// `apollo-design` theme wrapper — the hook `theme-variables.css` uses for colors. +// +// Delegating to the built-in formatter keeps the generated token body +// byte-identical; only the selector changes. +StyleDictionary.registerFormat({ + name: 'css/variables-apollo-design', + formatter: function (dictionary, platform) { + return StyleDictionary.format['css/variables'] + .call(this, dictionary, platform) + .replace(':root {', ':root, .apollo-design {'); + }, +}); + StyleDictionary.buildAllPlatforms(); diff --git a/packages/apollo-core/src/tokens/config.js b/packages/apollo-core/src/tokens/config.js index 729e0f513..b95407790 100644 --- a/packages/apollo-core/src/tokens/config.js +++ b/packages/apollo-core/src/tokens/config.js @@ -57,7 +57,7 @@ module.exports = { buildPath: 'src/tokens/css/', files: [ { - format: 'css/variables', + format: 'css/variables-apollo-design', destination: 'variables.css', options: { showFileHeader: false, From 0ee7ab7384e80a1eeb7c6ec2b22fb928b0fe83b3 Mon Sep 17 00:00:00 2001 From: David Rios Date: Thu, 16 Jul 2026 11:49:10 -0400 Subject: [PATCH 2/6] chore(apollo-core): remove orphaned src/tokens/build.js token generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/tokens/build.js was a stale duplicate of the root build-tokens.js (the script `build:tokens` actually runs) — superseded and never deleted, referenced by nothing but a biome ignore entry. It extends the same config.js, so after adding the css/variables-apollo-design format it would throw an unknown-format error if run directly, while drifting from the real generator. Delete it (and its biome ignore line) to remove the duplication. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/apollo-core/biome.json | 1 - packages/apollo-core/src/tokens/build.js | 31 ------------------------ 2 files changed, 32 deletions(-) delete mode 100644 packages/apollo-core/src/tokens/build.js diff --git a/packages/apollo-core/biome.json b/packages/apollo-core/biome.json index f50759dfa..8c70b32d1 100644 --- a/packages/apollo-core/biome.json +++ b/packages/apollo-core/biome.json @@ -13,7 +13,6 @@ "!**/fonts/**", "!**/src/icons/index.ts", "!**/*.css", - "!**/src/tokens/build.js", "!**/src/tokens/config.js", "!**/build-tokens.js" ] diff --git a/packages/apollo-core/src/tokens/build.js b/packages/apollo-core/src/tokens/build.js deleted file mode 100644 index 59847848e..000000000 --- a/packages/apollo-core/src/tokens/build.js +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ -const _ = require('lodash'); -const fs = require('fs'); -const StyleDictionary = require('style-dictionary').extend(require('./config')); - -StyleDictionary.registerFormat({ - name: 'palette', - formatter: _.template(fs.readFileSync(`${__dirname}/templates/palette.template`)), -}); - -StyleDictionary.registerFormat({ - name: 'scss/base-host', - formatter: _.template(fs.readFileSync(`${__dirname}/templates/base-host.template`)), -}); - -StyleDictionary.registerFormat({ - name: 'scss/theme-variables', - formatter: _.template(fs.readFileSync(`${__dirname}/templates/theme-variables.template`)), -}); - -StyleDictionary.registerFormat({ - name: 'scss/theme', - formatter: _.template(fs.readFileSync(`${__dirname}/templates/theme.template`)), -}); - -StyleDictionary.registerFormat({ - name: 'css/theme-variables', - formatter: _.template(fs.readFileSync(`${__dirname}/templates/css-theme-variables.template`)), -}); - -StyleDictionary.buildAllPlatforms(); From 11ff5b131e6aa01dc51e941d250a62f312f5f94e Mon Sep 17 00:00:00 2001 From: David Rios Date: Thu, 16 Jul 2026 17:48:47 -0400 Subject: [PATCH 3/6] fix(apollo-core): scope base tokens to :root, :host for shadow DOM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Emit variables.css base tokens (palette, fonts, radius) under `:root, :host` instead of `:root, .apollo-design`. `:host` matches any shadow host that adopts or injects the sheet, so base tokens resolve inside a shadow root — and inherit through the whole shadow tree, including portaled content — without requiring an `.apollo-design` wrapper. Light DOM is unaffected: `:root` still applies and `:host` matches nothing there. This fixes shadow-DOM consumers that self-inject the canvas sheet (traceview's web component, ap-chat) losing apollo fonts and falling back to Tailwind's `ui-monospace`, and lets `:host`-based consumers drop their runtime `:root`→`:host` selector rewrites. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/apollo-core/build-tokens.js | 13 ++++++++----- packages/apollo-core/src/tokens/config.js | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/apollo-core/build-tokens.js b/packages/apollo-core/build-tokens.js index e7b65ef73..0b4d71cae 100644 --- a/packages/apollo-core/build-tokens.js +++ b/packages/apollo-core/build-tokens.js @@ -35,18 +35,21 @@ StyleDictionary.registerFormat({ // (palette, fonts, radius) under bare `:root {}`, which never matches inside a // shadow root (`:root` = ; a shadow root is a DocumentFragment). // -// So shadow-DOM canvas consumers lose the fonts and fall back to Tailwind's -// `ui-monospace`. Emitting `:root, .apollo-design` too makes them apply via the -// `apollo-design` theme wrapper — the hook `theme-variables.css` uses for colors. +// So shadow-DOM canvas consumers (traceview's web component, ap-chat) lose the +// fonts and fall back to Tailwind's `ui-monospace`. Emitting `:root, :host` too +// makes the base tokens apply to any shadow host that adopts/injects this sheet; +// they then inherit through the whole shadow tree (including portaled content), +// with no `.apollo-design` wrapper required. In the light DOM `:root` still +// applies; `:host` simply matches nothing there. // // Delegating to the built-in formatter keeps the generated token body // byte-identical; only the selector changes. StyleDictionary.registerFormat({ - name: 'css/variables-apollo-design', + name: 'css/variables-shadow-host', formatter: function (dictionary, platform) { return StyleDictionary.format['css/variables'] .call(this, dictionary, platform) - .replace(':root {', ':root, .apollo-design {'); + .replace(':root {', ':root, :host {'); }, }); diff --git a/packages/apollo-core/src/tokens/config.js b/packages/apollo-core/src/tokens/config.js index b95407790..bfc155fe1 100644 --- a/packages/apollo-core/src/tokens/config.js +++ b/packages/apollo-core/src/tokens/config.js @@ -57,7 +57,7 @@ module.exports = { buildPath: 'src/tokens/css/', files: [ { - format: 'css/variables-apollo-design', + format: 'css/variables-shadow-host', destination: 'variables.css', options: { showFileHeader: false, From 7a8fa1dd6e00fb01fb03f27891db96f1a32094ca Mon Sep 17 00:00:00 2001 From: David Rios Date: Thu, 16 Jul 2026 18:01:55 -0400 Subject: [PATCH 4/6] fix(ap-chat): adopt apollo tokens without shadow-DOM selector rewriting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that apollo-core scopes base tokens to `:root, :host` (the canvas shadow-DOM fix), ap-chat no longer rewrites `:root`→`:host` or `body.`→`:host(.) *` at runtime. It adopts variables.css and theme-variables.css raw: - base tokens (fonts/palette/radius) resolve via `:host` on the element and inherit through the whole shadow tree, including the portal; - semantic theme tokens resolve via apollo's existing `:where(.:not(.react-flow))`, which now matches the portal container too — it gains the theme class alongside the React container. Co-Authored-By: Claude Opus 4.8 (1M context) --- web-packages/ap-chat/src/ap-chat-element.ts | 45 ++++++++++----------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/web-packages/ap-chat/src/ap-chat-element.ts b/web-packages/ap-chat/src/ap-chat-element.ts index d778014d9..a2969a658 100644 --- a/web-packages/ap-chat/src/ap-chat-element.ts +++ b/web-packages/ap-chat/src/ap-chat-element.ts @@ -2,14 +2,12 @@ // These load into document and are available to Shadow DOM import '@uipath/apollo-react/core/fonts/font.css'; -import { createElement } from 'react'; - -import { createRoot, type Root } from 'react-dom/client'; - // Import Apollo CSS variables as raw strings for Shadow DOM injection // Using ?raw query parameter to import CSS as text instead of injecting globally import apolloThemeVariablesCSS from '@uipath/apollo-react/core/tokens/css/theme-variables.css?raw'; import apolloVariablesCSS from '@uipath/apollo-react/core/tokens/css/variables.css?raw'; +import { createElement } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; import { cleanupReactRenderer, createReactRenderer } from './react-renderer'; import { AutopilotChatEvent, type AutopilotChatService } from './service'; @@ -245,24 +243,18 @@ export class ApChat extends HTMLElement { } `; - // Create a constructable stylesheet for Shadow DOM - // Include Apollo design tokens, font faces, and icon styles - // Transform CSS selectors to work in Shadow DOM context - const transformedVariablesCSS = apolloVariablesCSS.replace(/:root\s*\{/g, ':host {'); // Transform :root to :host for Shadow DOM - - // For theme variables, apply to both :host and all children to ensure proper cascading - // This ensures theme variables are available everywhere in the shadow tree - const transformedThemeVariablesCSS = apolloThemeVariablesCSS - .replace(/:root\s*\{/g, ':host {') - .replace( - /body\.(light|dark|light-hc|dark-hc)(?:,\s*\.apollo-design\.\1)?\s*\{/g, - ':host(.$1), :host(.$1) * {' - ); // Apply to host and all descendants - + // Adopt Apollo design tokens, font faces, and icon styles into the Shadow DOM. + // No selector rewriting is needed: + // - `variables.css` base tokens (palette, fonts, radius) are scoped to + // `:root, :host` by apollo-core, so `:host` sets them on this element and + // they inherit through the entire shadow tree (including the portal). + // - `theme-variables.css` semantic tokens match any themed element via + // `:where(.light:not(.react-flow))`, which covers the container and portal + // container below (both carry the theme class). const shadowStyleSheet = new CSSStyleSheet(); const allStyles = [ - transformedVariablesCSS, - transformedThemeVariablesCSS, + apolloVariablesCSS, + apolloThemeVariablesCSS, ...fontFaceRules, iconStylesText, ].join('\n'); @@ -288,7 +280,9 @@ export class ApChat extends HTMLElement { `; this.shadowRoot.appendChild(hostStyles); - // Apply theme class to host element for :host(.theme) selectors + // Theme class on the host is a convenience hook for external styling; apollo + // tokens no longer depend on it (base via :host, semantic via the themed + // container/portal below). this.className = this._theme || 'light'; // Create container for React @@ -300,9 +294,9 @@ export class ApChat extends HTMLElement { this.shadowRoot.appendChild(this.container); // Create dedicated portal container for tooltips/popovers at Shadow DOM root - // This prevents clipping in embedded mode while being a real HTMLElement for MUI + // This prevents clipping in embedded mode while being a real HTMLElement for MUI. this.portalContainer = document.createElement('div'); - this.portalContainer.className = 'portal-container'; + this.portalContainer.className = `portal-container ${this._theme || 'light'}`; this.portalContainer.style.position = 'fixed'; this.portalContainer.style.top = '0'; this.portalContainer.style.left = '0'; @@ -373,10 +367,13 @@ export class ApChat extends HTMLElement { // Update theme class on both host element and container const theme = this._theme || 'light'; - this.className = theme; // For :host(.theme) selectors in Shadow DOM + this.className = theme; if (this.container) { this.container.className = theme; } + if (this.portalContainer) { + this.portalContainer.className = `portal-container ${theme}`; + } const props: ApChatProperties = { chatServiceInstance: this._chatServiceInstance, From 9b3ed7ee146d0c7df671bee6625f36d0d52734c0 Mon Sep 17 00:00:00 2001 From: David Rios Date: Thu, 16 Jul 2026 18:05:20 -0400 Subject: [PATCH 5/6] docs(apollo-core): trim the :root, :host formatter comment Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/apollo-core/build-tokens.js | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/apollo-core/build-tokens.js b/packages/apollo-core/build-tokens.js index 0b4d71cae..5a6777cc8 100644 --- a/packages/apollo-core/build-tokens.js +++ b/packages/apollo-core/build-tokens.js @@ -31,19 +31,11 @@ StyleDictionary.registerFormat({ formatter: _.template(fs.readFileSync(path.join(templateDir, 'css-theme-variables.template'))), }); -// The built-in `css/variables` format emits `variables.css`'s base tokens -// (palette, fonts, radius) under bare `:root {}`, which never matches inside a -// shadow root (`:root` = ; a shadow root is a DocumentFragment). -// -// So shadow-DOM canvas consumers (traceview's web component, ap-chat) lose the -// fonts and fall back to Tailwind's `ui-monospace`. Emitting `:root, :host` too -// makes the base tokens apply to any shadow host that adopts/injects this sheet; -// they then inherit through the whole shadow tree (including portaled content), -// with no `.apollo-design` wrapper required. In the light DOM `:root` still -// applies; `:host` simply matches nothing there. -// -// Delegating to the built-in formatter keeps the generated token body -// byte-identical; only the selector changes. +// `css/variables` emits base tokens (palette, fonts, radius) under bare `:root {}`, +// which never matches inside a shadow root — so shadow-DOM consumers (traceview WC, +// ap-chat) fall back to Tailwind defaults. Adding `:host` scopes the tokens to the +// shadow host too (inherited through the tree); `:root` still covers the light DOM. +// Delegating to the built-in formatter keeps the token body byte-identical. StyleDictionary.registerFormat({ name: 'css/variables-shadow-host', formatter: function (dictionary, platform) { From 54ad6c8233616840ca145d53e0c2133627b47ab5 Mon Sep 17 00:00:00 2001 From: David Rios Date: Thu, 16 Jul 2026 18:51:37 -0400 Subject: [PATCH 6/6] test(ap-chat): assert portal theme class; fail loud on selector no-op - ap-chat: add a test that the portal container carries the theme class initially and keeps it in sync when the theme changes, guarding the :where(.light) semantic-token activation the portal subtree relies on. - apollo-core: throw in css/variables-shadow-host if the ':root {' selector isn't found, so a future Style Dictionary reformat can't silently no-op and regenerate variables.css back to bare :root, reintroducing the shadow-DOM token-scoping bug. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/apollo-core/build-tokens.js | 16 +++++++++++--- .../src/__tests__/ap-chat-element.test.ts | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/apollo-core/build-tokens.js b/packages/apollo-core/build-tokens.js index 5a6777cc8..5fab55308 100644 --- a/packages/apollo-core/build-tokens.js +++ b/packages/apollo-core/build-tokens.js @@ -39,9 +39,19 @@ StyleDictionary.registerFormat({ StyleDictionary.registerFormat({ name: 'css/variables-shadow-host', formatter: function (dictionary, platform) { - return StyleDictionary.format['css/variables'] - .call(this, dictionary, platform) - .replace(':root {', ':root, :host {'); + const css = StyleDictionary.format['css/variables'].call(this, dictionary, platform); + const scoped = css.replace(':root {', ':root, :host {'); + // Fail loud if the selector didn't change: a silent no-op (e.g. if a future + // Style Dictionary version emits `:root{` or reformats the block) would + // regenerate `variables.css` back to bare `:root {}` and reintroduce the + // shadow-DOM token-scoping bug this format exists to fix, with no signal. + if (scoped === css) { + throw new Error( + "css/variables-shadow-host: expected ':root {' in the built-in css/variables output but found none — " + + 'the selector was not rewritten to :root, :host. Shadow-DOM base tokens would be lost.' + ); + } + return scoped; }, }); diff --git a/web-packages/ap-chat/src/__tests__/ap-chat-element.test.ts b/web-packages/ap-chat/src/__tests__/ap-chat-element.test.ts index 950aaf839..bfc951d94 100644 --- a/web-packages/ap-chat/src/__tests__/ap-chat-element.test.ts +++ b/web-packages/ap-chat/src/__tests__/ap-chat-element.test.ts @@ -439,6 +439,27 @@ describe('ApChat Web Component', () => { expect(portalContainerAfter).toBe(portalContainer); }); + it('should carry the theme class on the portal container and keep it in sync', () => { + element.chatServiceInstance = mockService; + element.connectedCallback(); + + const shadowRoot = element.shadowRoot; + const portalContainer = shadowRoot?.querySelector('.portal-container') as HTMLDivElement; + expect(portalContainer).not.toBeNull(); + + // theme-variables.css activates semantic tokens for the portal subtree via + // selectors like `:where(.light)`, so the portal container must carry the + // theme class initially and update when the theme changes. + expect(portalContainer.classList.contains('light')).toBe(true); // default theme + + element.theme = 'dark'; + + expect(portalContainer.classList.contains('dark')).toBe(true); + expect(portalContainer.classList.contains('light')).toBe(false); + // The base `portal-container` class is preserved across theme changes. + expect(portalContainer.classList.contains('portal-container')).toBe(true); + }); + it('should maintain portal container across locale changes', () => { element.chatServiceInstance = mockService; element.connectedCallback();