Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

inject app theme into plugin instead of styles #2703

Merged
merged 2 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
hexToRgbStr,
ICustomTheme,
ITheme,
getCSS,
} from 'altair-graphql-core/build/theme';

import { css } from '@emotion/css';
Expand All @@ -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);
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
10 changes: 8 additions & 2 deletions packages/altair-core/src/plugin/v3/panel.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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(`
Expand All @@ -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;
}
Expand Down
38 changes: 27 additions & 11 deletions packages/altair-core/src/plugin/v3/parent-engine.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ICustomTheme } from '../../theme';
import { CreateActionOptions } from '../context/context.interface';
import { PluginEventPayloadMap } from '../event/event.interfaces';
import { PluginV3Context } from './context';
Expand All @@ -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;
Expand Down Expand Up @@ -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 };
});
}

Expand Down
115 changes: 115 additions & 0 deletions packages/altair-core/src/theme/css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import lightTheme from './defaults/light';
import darkTheme from './defaults/dark';
import { ICustomTheme, ITheme, createTheme, hexToRgbStr } from './theme';

const asCSSVariablesString = (theme: ITheme) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider refactoring CSS variable generation into structured mapping objects with type-safe accessors

The CSS variable generation can be simplified while maintaining explicit mapping. Here's how:

const COLOR_VARS = {
  // Base colors
  'black-color': (t) => t.colors.black,
  'dark-grey-color': (t) => t.colors.darkGray,
  // ... other colors
} as const;

const RGB_VARS = {
  'rgb-black': (t) => hexToRgbStr(t.colors.black),
  'rgb-dark-grey': (t) => hexToRgbStr(t.colors.darkGray),
  // ... other rgb values
} as const;

const asCSSVariablesString = (theme: ITheme) => {
  const createVars = (mapping: Record<string, (t: ITheme) => string>) => 
    Object.entries(mapping)
      .map(([key, getValue]) => `--${key}: ${getValue(theme)};`)
      .join('\n    ');

  return `
  :root {
    ${createVars(COLOR_VARS)}
    ${createVars(RGB_VARS)}
    // Other variable groups...
  }`;
};

This approach:

  • Groups related variables into mapping objects
  • Maintains explicit relationships between theme properties and CSS variables
  • Reduces repetition and chance of errors
  • Makes adding new variables easier
  • Keeps type safety with const assertions

return `
:root {
--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};
}
`;
};

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));
};
1 change: 1 addition & 0 deletions packages/altair-core/src/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading