diff --git a/packages/altair-app/src/app/modules/altair/directives/theme/theme.directive.spec.ts b/packages/altair-app/src/app/modules/altair/directives/theme/theme.directive.spec.ts index 21a6406883..47901714aa 100644 --- a/packages/altair-app/src/app/modules/altair/directives/theme/theme.directive.spec.ts +++ b/packages/altair-app/src/app/modules/altair/directives/theme/theme.directive.spec.ts @@ -3,7 +3,7 @@ import { mock } from '../../../../../testing'; describe('ThemeDirective', () => { it('should create an instance', () => { - const directive = new ThemeDirective(mock(), mock()); + const directive = new ThemeDirective(mock()); expect(directive).toBeTruthy(); }); }); diff --git a/packages/altair-app/src/app/modules/altair/directives/theme/theme.directive.ts b/packages/altair-app/src/app/modules/altair/directives/theme/theme.directive.ts index 5ed169d8bc..8fe370ec77 100644 --- a/packages/altair-app/src/app/modules/altair/directives/theme/theme.directive.ts +++ b/packages/altair-app/src/app/modules/altair/directives/theme/theme.directive.ts @@ -4,6 +4,7 @@ import { hexToRgbStr, ICustomTheme, ITheme, + getCSS, } from 'altair-graphql-core/build/theme'; import { css } from '@emotion/css'; @@ -20,10 +21,7 @@ export class ThemeDirective implements OnInit, OnChanges { private className = ''; - constructor( - private themeRegistry: ThemeRegistryService, - private nzConfigService: NzConfigService - ) {} + constructor(private nzConfigService: NzConfigService) {} ngOnInit() { this.applyTheme(this.appTheme, this.appDarkTheme, this.appAccentColor); @@ -43,118 +41,12 @@ export class ThemeDirective implements OnInit, OnChanges { } } - getCssString(theme: ITheme) { - return ` - --baseline-size: ${theme.type.fontSize.base}; - --rem-base: ${theme.type.fontSize.remBase}; - --body-font-size: ${theme.type.fontSize.body}; - - --app-easing: ${theme.easing}; - - --black-color: ${theme.colors.black}; - --dark-grey-color: ${theme.colors.darkGray}; - --grey-color: ${theme.colors.gray}; - --light-grey-color: ${theme.colors.lightGray}; - --white-color: ${theme.colors.white}; - --green-color: ${theme.colors.green}; - --blue-color: ${theme.colors.blue}; - --cerise-color: ${theme.colors.cerise}; - --red-color: ${theme.colors.red}; - --rose-color: ${theme.colors.rose}; - --orange-color: ${theme.colors.orange}; - --yellow-color: ${theme.colors.yellow}; - --light-red-color: ${theme.colors.lightRed}; - --dark-purple-color: ${theme.colors.darkPurple}; - - --primary-color: ${theme.colors.primary}; - --secondary-color: ${theme.colors.secondary}; - --tertiary-color: ${theme.colors.tertiary}; - - --shadow-bg: rgba(${hexToRgbStr(theme.shadow.color)}, ${theme.shadow.opacity}); - - --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", 'Helvetica Neue', Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - - --rgb-black: ${hexToRgbStr(theme.colors.black)}; - --rgb-dark-grey: ${hexToRgbStr(theme.colors.darkGray)}; - --rgb-grey: ${hexToRgbStr(theme.colors.gray)}; - --rgb-light-grey: ${hexToRgbStr(theme.colors.lightGray)}; - --rgb-white: ${hexToRgbStr(theme.colors.white)}; - --rgb-green: ${hexToRgbStr(theme.colors.green)}; - --rgb-blue: ${hexToRgbStr(theme.colors.blue)}; - --rgb-cerise: ${hexToRgbStr(theme.colors.cerise)}; - --rgb-red: ${hexToRgbStr(theme.colors.red)}; - --rgb-orange: ${hexToRgbStr(theme.colors.orange)}; - --rgb-yellow: ${hexToRgbStr(theme.colors.yellow)}; - --rgb-light-red: ${hexToRgbStr(theme.colors.lightRed)}; - --rgb-dark-purple: ${hexToRgbStr(theme.colors.darkPurple)}; - - --editor-font-family: ${theme.editor.fontFamily.default}; - --editor-font-size: ${theme.editor.fontSize}; - - --theme-bg-color: ${theme.colors.bg}; - --theme-off-bg-color: ${theme.colors.offBg}; - --theme-font-color: ${theme.colors.font}; - --theme-off-font-color: ${theme.colors.offFont}; - --theme-border-color: ${theme.colors.border}; - --theme-off-border-color: ${theme.colors.offBorder}; - --header-bg-color: ${theme.colors.headerBg || theme.colors.offBg}; - - --rgb-primary: ${hexToRgbStr(theme.colors.primary)}; - --rgb-secondary: ${hexToRgbStr(theme.colors.secondary)}; - --rgb-tertiary: ${hexToRgbStr(theme.colors.tertiary)}; - - --rgb-theme-bg: ${hexToRgbStr(theme.colors.bg)}; - --rgb-theme-off-bg: ${hexToRgbStr(theme.colors.offBg)}; - --rgb-theme-font: ${hexToRgbStr(theme.colors.font)}; - --rgb-theme-off-font: ${hexToRgbStr(theme.colors.offFont)}; - --rgb-theme-border: ${hexToRgbStr(theme.colors.border)}; - --rgb-theme-off-border: ${hexToRgbStr(theme.colors.offBorder)}; - --rgb-header-bg: ${hexToRgbStr(theme.colors.headerBg || theme.colors.offBg)}; - - --editor-comment-color: ${theme.editor.colors.comment}; - --editor-string-color: ${theme.editor.colors.string}; - --editor-number-color: ${theme.editor.colors.number}; - --editor-variable-color: ${theme.editor.colors.variable}; - --editor-attribute-color: ${theme.editor.colors.attribute}; - --editor-keyword-color: ${theme.editor.colors.keyword}; - --editor-atom-color: ${theme.editor.colors.atom}; - --editor-property-color: ${theme.editor.colors.property}; - --editor-punctuation-color: ${theme.editor.colors.punctuation}; - --editor-cursor-color: ${theme.editor.colors.cursor}; - --editor-def-color: ${theme.editor.colors.definition}; - --editor-builtin-color: ${theme.editor.colors.builtin}; - `; - } - getDynamicClassName( appTheme: ICustomTheme, appDarkTheme?: ICustomTheme, accentColor?: string ) { - const extraTheme = accentColor ? { colors: { primary: accentColor } } : {}; - if (appTheme && appDarkTheme) { - return css(` - ${this.getCssString(createTheme(appTheme, extraTheme))} - @media (prefers-color-scheme: dark) { - ${this.getCssString(createTheme(appDarkTheme, extraTheme))} - } - `); - } - - if (!appTheme || appTheme.isSystem) { - return css(` - ${this.getCssString( - createTheme(this.themeRegistry.getTheme('light')!, appTheme, extraTheme) - )} - @media (prefers-color-scheme: dark) { - ${this.getCssString( - createTheme(this.themeRegistry.getTheme('dark')!, appTheme, extraTheme) - )} - } - `); - } - - return css(this.getCssString(createTheme(appTheme, extraTheme))); + return css(getCSS(appTheme, appDarkTheme, accentColor)); } applyTheme(theme: ICustomTheme, darkTheme?: ICustomTheme, accentColor?: string) { diff --git a/packages/altair-app/src/app/modules/altair/services/plugin/context/plugin-context.service.ts b/packages/altair-app/src/app/modules/altair/services/plugin/context/plugin-context.service.ts index abb26f3aa9..0ba4e0dc07 100644 --- a/packages/altair-app/src/app/modules/altair/services/plugin/context/plugin-context.service.ts +++ b/packages/altair-app/src/app/modules/altair/services/plugin/context/plugin-context.service.ts @@ -314,7 +314,19 @@ export class PluginContextService implements PluginContextGenerator { const sanitized = DOMPurify.default.sanitize(manifest.icon.src); iconSvg = self.sanitizer.bypassSecurityTrustHtml(sanitized); } - const engine = new PluginParentEngine(panelWorker); + + const settings = await firstValueFrom( + self.store.select('settings').pipe(take(1)) + ); + const selectedTheme = self.themeRegistryService.getTheme(settings.theme) || { + isSystem: true, + }; + const settingsThemeConfig = settings.themeConfig || {}; + const theme = self.themeRegistryService.mergeThemes( + selectedTheme, + settingsThemeConfig + ); + const engine = new PluginParentEngine(panelWorker, { theme }); const panel = new AltairPanel( title, panelWorker.getIframe(), diff --git a/packages/altair-core/src/plugin/v3/panel.ts b/packages/altair-core/src/plugin/v3/panel.ts index e0746506bf..9effde1c02 100644 --- a/packages/altair-core/src/plugin/v3/panel.ts +++ b/packages/altair-core/src/plugin/v3/panel.ts @@ -1,9 +1,11 @@ +import { ICustomTheme, getCSS } from '../../theme'; import { PluginV3Context } from './context'; interface StylesData { styleUrls: string[]; styles: string[]; htmlClasses: string[]; + theme?: ICustomTheme; } export abstract class AltairV3Panel { abstract create(ctx: PluginV3Context, container: HTMLElement): void; @@ -35,7 +37,11 @@ export abstract class AltairV3Panel { document.head.appendChild(link); }); - this.injectCSS(data.styles.join('\n')); + if (data.styles.length) { + this.injectCSS(data.styles.join('\n')); + } else if (data.theme) { + this.injectCSS(getCSS(data.theme)); + } // set the background color of the panel to the theme background color this.injectCSS(` @@ -51,7 +57,7 @@ export abstract class AltairV3Panel { private injectCSS(css: string) { let el = document.createElement('style'); - el.innerText = css; + el.innerText = css.replace(/[\n\r]/g, ''); document.head.appendChild(el); return el; } diff --git a/packages/altair-core/src/plugin/v3/parent-engine.ts b/packages/altair-core/src/plugin/v3/parent-engine.ts index 71a26b3e3d..094ffd957e 100644 --- a/packages/altair-core/src/plugin/v3/parent-engine.ts +++ b/packages/altair-core/src/plugin/v3/parent-engine.ts @@ -1,3 +1,4 @@ +import { ICustomTheme } from '../../theme'; import { CreateActionOptions } from '../context/context.interface'; import { PluginEventPayloadMap } from '../event/event.interfaces'; import { PluginV3Context } from './context'; @@ -20,11 +21,33 @@ const mainInstanceOnlyEvents: (keyof PluginV3Context)[] = [ // methods to be excluded from the generic listener creation since they are handled specially const speciallyHandledMethods: (keyof PluginV3Context)[] = ['on', 'createAction']; +function getCssStyles(relevantClasses: string[]) { + try { + const styleSheets = Array.from(document.styleSheets); + return styleSheets + .map((sheet) => { + return Array.from(sheet.cssRules) + .map((rule) => rule.cssText) + .join(''); + }) + .filter((css) => { + return relevantClasses.some((htmlClass) => css.includes(`.${htmlClass}`)); + }); + } catch { + return []; + } +} +interface PluginParentEngineOptions { + theme?: ICustomTheme; +} export class PluginParentEngine { private context?: PluginV3Context; subscribedEvents: string[] = []; - constructor(private worker: PluginParentWorker) {} + constructor( + private worker: PluginParentWorker, + private opts?: PluginParentEngineOptions + ) {} start(context: PluginV3Context) { this.context = context; @@ -101,17 +124,10 @@ export class PluginParentEngine { const htmlClasses = Array.from(document.documentElement.classList); // Get the styles that are applicable to the current theme of the page - const styles = styleSheets - .map((sheet) => { - return Array.from(sheet.cssRules) - .map((rule) => rule.cssText) - .join(''); - }) - .filter((css) => { - return htmlClasses.some((htmlClass) => css.includes(`.${htmlClass}`)); - }); + // Doesn't work crossorigin cases. e.g. when loading from CDN. Fallback to theme instead. + const styles = getCssStyles(htmlClasses); - return { styleUrls, styles, htmlClasses }; + return { styleUrls, styles, htmlClasses, theme: this.opts?.theme }; }); } diff --git a/packages/altair-core/src/theme/css.ts b/packages/altair-core/src/theme/css.ts new file mode 100644 index 0000000000..76dd420278 --- /dev/null +++ b/packages/altair-core/src/theme/css.ts @@ -0,0 +1,130 @@ +import lightTheme from './defaults/light'; +import darkTheme from './defaults/dark'; +import { ICustomTheme, ITheme, createTheme, hexToRgbStr } from './theme'; + +const COLOR_VARS = { + // Base colors + 'black-color': (t: ITheme) => t.colors.black, + 'dark-grey-color': (t: ITheme) => t.colors.darkGray, + 'grey-color': (t: ITheme) => t.colors.gray, + 'light-grey-color': (t: ITheme) => t.colors.lightGray, + 'white-color': (t: ITheme) => t.colors.white, + 'green-color': (t: ITheme) => t.colors.green, + 'blue-color': (t: ITheme) => t.colors.blue, + 'cerise-color': (t: ITheme) => t.colors.cerise, + 'red-color': (t: ITheme) => t.colors.red, + 'rose-color': (t: ITheme) => t.colors.rose, + 'orange-color': (t: ITheme) => t.colors.orange, + 'yellow-color': (t: ITheme) => t.colors.yellow, + 'light-red-color': (t: ITheme) => t.colors.lightRed, + 'dark-purple-color': (t: ITheme) => t.colors.darkPurple, + + 'primary-color': (t: ITheme) => t.colors.primary, + 'secondary-color': (t: ITheme) => t.colors.secondary, + 'tertiary-color': (t: ITheme) => t.colors.tertiary, + + 'theme-bg-color': (t: ITheme) => t.colors.bg, + 'theme-off-bg-color': (t: ITheme) => t.colors.offBg, + 'theme-font-color': (t: ITheme) => t.colors.font, + 'theme-off-font-color': (t: ITheme) => t.colors.offFont, + 'theme-border-color': (t: ITheme) => t.colors.border, + 'theme-off-border-color': (t: ITheme) => t.colors.offBorder, + 'header-bg-color': (t: ITheme) => t.colors.headerBg || t.colors.offBg, + + 'editor-comment-color': (t: ITheme) => t.editor.colors.comment, + 'editor-string-color': (t: ITheme) => t.editor.colors.string, + 'editor-number-color': (t: ITheme) => t.editor.colors.number, + 'editor-variable-color': (t: ITheme) => t.editor.colors.variable, + 'editor-attribute-color': (t: ITheme) => t.editor.colors.attribute, + 'editor-keyword-color': (t: ITheme) => t.editor.colors.keyword, + 'editor-atom-color': (t: ITheme) => t.editor.colors.atom, + 'editor-property-color': (t: ITheme) => t.editor.colors.property, + 'editor-punctuation-color': (t: ITheme) => t.editor.colors.punctuation, + 'editor-cursor-color': (t: ITheme) => t.editor.colors.cursor, + 'editor-def-color': (t: ITheme) => t.editor.colors.definition, + 'editor-builtin-color': (t: ITheme) => t.editor.colors.builtin, +} as const; + +const RGB_VARS = { + 'rgb-black': (t: ITheme) => hexToRgbStr(t.colors.black), + 'rgb-dark-grey': (t: ITheme) => hexToRgbStr(t.colors.darkGray), + 'rgb-grey': (t: ITheme) => hexToRgbStr(t.colors.gray), + 'rgb-light-grey': (t: ITheme) => hexToRgbStr(t.colors.lightGray), + 'rgb-white': (t: ITheme) => hexToRgbStr(t.colors.white), + 'rgb-green': (t: ITheme) => hexToRgbStr(t.colors.green), + 'rgb-blue': (t: ITheme) => hexToRgbStr(t.colors.blue), + 'rgb-cerise': (t: ITheme) => hexToRgbStr(t.colors.cerise), + 'rgb-red': (t: ITheme) => hexToRgbStr(t.colors.red), + 'rgb-rose': (t: ITheme) => hexToRgbStr(t.colors.rose), + 'rgb-orange': (t: ITheme) => hexToRgbStr(t.colors.orange), + 'rgb-yellow': (t: ITheme) => hexToRgbStr(t.colors.yellow), + 'rgb-light-red': (t: ITheme) => hexToRgbStr(t.colors.lightRed), + 'rgb-dark-purple': (t: ITheme) => hexToRgbStr(t.colors.darkPurple), + + 'rgb-primary': (t: ITheme) => hexToRgbStr(t.colors.primary), + 'rgb-secondary': (t: ITheme) => hexToRgbStr(t.colors.secondary), + 'rgb-tertiary': (t: ITheme) => hexToRgbStr(t.colors.tertiary), + + 'rgb-theme-bg': (t: ITheme) => hexToRgbStr(t.colors.bg), + 'rgb-theme-off-bg': (t: ITheme) => hexToRgbStr(t.colors.offBg), + 'rgb-theme-font': (t: ITheme) => hexToRgbStr(t.colors.font), + 'rgb-theme-off-font': (t: ITheme) => hexToRgbStr(t.colors.offFont), + 'rgb-theme-border': (t: ITheme) => hexToRgbStr(t.colors.border), + 'rgb-theme-off-border': (t: ITheme) => hexToRgbStr(t.colors.offBorder), + 'rgb-header-bg': (t: ITheme) => hexToRgbStr(t.colors.headerBg || t.colors.offBg), + // ... other rgb values +} as const; + +const createVars = (mapping: Record string>, theme: ITheme) => + Object.entries(mapping) + .map(([key, getValue]) => `--${key}: ${getValue(theme)};`) + .join('\n '); + +const asCSSVariablesString = (theme: ITheme) => { + return ` + :root { + --shadow-bg: rgba(${hexToRgbStr(theme.shadow.color)}, ${theme.shadow.opacity}); + + --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", 'Helvetica Neue', Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + + --editor-font-family: ${theme.editor.fontFamily.default}; + --editor-font-size: ${theme.editor.fontSize}; + + --baseline-size: ${theme.type.fontSize.base}; + --rem-base: ${theme.type.fontSize.remBase}; + --body-font-size: ${theme.type.fontSize.body}; + + --app-easing: ${theme.easing}; + + ${createVars(COLOR_VARS, theme)} + ${createVars(RGB_VARS, theme)} + } + `; +}; + +export const getCSS = ( + appTheme: ICustomTheme, + appDarkTheme?: ICustomTheme, + accentColor?: string +) => { + const extraTheme = accentColor ? { colors: { primary: accentColor } } : {}; + if (appTheme && appDarkTheme) { + return ` + ${asCSSVariablesString(createTheme(appTheme, extraTheme))} + @media (prefers-color-scheme: dark) { + ${asCSSVariablesString(createTheme(appDarkTheme, extraTheme))} + } + `; + } + + if (!appTheme || appTheme.isSystem) { + return ` + ${asCSSVariablesString(createTheme(lightTheme, appTheme, extraTheme))} + @media (prefers-color-scheme: dark) { + ${asCSSVariablesString(createTheme(darkTheme, appTheme, extraTheme))} + } + `; + } + + return asCSSVariablesString(createTheme(appTheme, extraTheme)); +}; diff --git a/packages/altair-core/src/theme/index.ts b/packages/altair-core/src/theme/index.ts index 6caf234505..e0223e7f32 100644 --- a/packages/altair-core/src/theme/index.ts +++ b/packages/altair-core/src/theme/index.ts @@ -3,6 +3,7 @@ import darkTheme from './defaults/dark'; import draculaTheme from './defaults/dracula'; export * from './theme'; +export * from './css'; export const light = lightTheme; export const dark = darkTheme; export const dracula = draculaTheme;