Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/apollo-core/biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"!**/fonts/**",
"!**/src/icons/index.ts",
"!**/*.css",
"!**/src/tokens/build.js",
"!**/src/tokens/config.js",
"!**/build-tokens.js"
]
Expand Down
24 changes: 24 additions & 0 deletions packages/apollo-core/build-tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,30 @@ StyleDictionary.registerFormat({
formatter: _.template(fs.readFileSync(path.join(templateDir, 'css-theme-variables.template'))),
});

// `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.
Comment thread
david-rios-uipath marked this conversation as resolved.
StyleDictionary.registerFormat({
name: 'css/variables-shadow-host',
formatter: function (dictionary, platform) {
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;
},
Comment thread
david-rios-uipath marked this conversation as resolved.
});

StyleDictionary.buildAllPlatforms();


31 changes: 0 additions & 31 deletions packages/apollo-core/src/tokens/build.js

This file was deleted.

2 changes: 1 addition & 1 deletion packages/apollo-core/src/tokens/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ module.exports = {
buildPath: 'src/tokens/css/',
files: [
{
format: 'css/variables',
format: 'css/variables-shadow-host',
destination: 'variables.css',
options: {
showFileHeader: false,
Expand Down
21 changes: 21 additions & 0 deletions web-packages/ap-chat/src/__tests__/ap-chat-element.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
45 changes: 21 additions & 24 deletions web-packages/ap-chat/src/ap-chat-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand All @@ -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
Expand All @@ -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';
Comment thread
david-rios-uipath marked this conversation as resolved.
this.portalContainer.style.top = '0';
this.portalContainer.style.left = '0';
Expand Down Expand Up @@ -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,
Expand Down
Loading