// @flow import Color from 'color'; import { render, THROW_ON_ERROR } from '../common/render'; import { getThemes } from './index'; import type { Theme } from './index'; import { getAppDefaultTheme } from '../common/constants'; type ThemeBlock = { background?: { default: string, success?: string, notice?: string, warning?: string, danger?: string, surprise?: string, info?: string, }, foreground?: { default: string, success?: string, notice?: string, warning?: string, danger?: string, surprise?: string, info?: string, }, highlight?: { default: string, xxs?: string, xs?: string, sm?: string, md?: string, lg?: string, xl?: string, }, }; type ThemeInner = { ...ThemeBlock, rawCss?: string, styles: ?{ dialog?: ThemeBlock, dialogFooter?: ThemeBlock, dialogHeader?: ThemeBlock, dropdown?: ThemeBlock, editor?: ThemeBlock, link?: ThemeBlock, overlay?: ThemeBlock, pane?: ThemeBlock, paneHeader?: ThemeBlock, sidebar?: ThemeBlock, sidebarHeader?: ThemeBlock, sidebarList?: ThemeBlock, tooltip?: ThemeBlock, transparentOverlay?: ThemeBlock, }, }; export type PluginTheme = { name: string, displayName: string, theme: ThemeInner, }; export async function generateThemeCSS(theme: PluginTheme): Promise { const renderedTheme: ThemeInner = await render( theme.theme, theme.theme, null, THROW_ON_ERROR, theme.name, ); const n = theme.name; let css = ''; // For the top-level variables, merge with the base theme to ensure that // we have everything we need. css += wrapStyles( n, '', getThemeBlockCSS({ ...renderedTheme, background: { ..._baseTheme.background, ...renderedTheme.background }, foreground: { ..._baseTheme.foreground, ...renderedTheme.foreground }, highlight: { ..._baseTheme.highlight, ...renderedTheme.highlight }, }), ); if (renderedTheme.styles) { const styles = renderedTheme.styles; // Dropdown Menus css += wrapStyles( n, '.theme--dropdown__menu', getThemeBlockCSS(styles.dropdown || styles.dialog), ); // Tooltips css += wrapStyles(n, '.theme--tooltip', getThemeBlockCSS(styles.tooltip || styles.dialog)); // Overlay css += wrapStyles( n, '.theme--transparent-overlay', getThemeBlockCSS(styles.transparentOverlay), ); // Dialogs css += wrapStyles(n, '.theme--dialog', getThemeBlockCSS(styles.dialog)); css += wrapStyles(n, '.theme--dialog__header', getThemeBlockCSS(styles.dialogHeader)); css += wrapStyles(n, '.theme--dialog__footer', getThemeBlockCSS(styles.dialogFooter)); // Panes css += wrapStyles(n, '.theme--pane', getThemeBlockCSS(styles.pane)); css += wrapStyles(n, '.theme--pane__header', getThemeBlockCSS(styles.paneHeader)); // Sidebar Styles css += wrapStyles(n, '.theme--sidebar', getThemeBlockCSS(styles.sidebar)); css += wrapStyles(n, '.theme--sidebar__list', getThemeBlockCSS(styles.sidebarList)); css += wrapStyles(n, '.theme--sidebar__header', getThemeBlockCSS(styles.sidebarHeader)); // Link css += wrapStyles(n, '.theme--link', getThemeBlockCSS(styles.link)); // Code Editors css += wrapStyles(n, '.theme--editor', getThemeBlockCSS(styles.editor)); // HACK: Dialog styles for CodeMirror dialogs too css += wrapStyles(n, '.CodeMirror-info', getThemeBlockCSS(styles.dialog)); } return css; } function getThemeBlockCSS(block?: ThemeBlock): string { if (!block) { return ''; } const indent = '\t'; let css = ''; const addColorVar = (variable: string, value?: string) => { if (!value) { return; } try { const parsedColor = Color(value); const rgb = parsedColor.rgb(); addVar(variable, rgb.string()); addVar(`${variable}-rgb`, rgb.array().join(', ')); } catch (err) { console.log('Failed to parse theme color', value); } }; const addVar = (variable: string, value?: string) => { if (!value) { return; } css += `${indent}--${variable}: ${value};\n`; }; const addComment = comment => { css += `${indent}/* ${comment} */\n`; }; const addNewLine = () => { css += '\n'; }; if (block.background) { const { background } = block; addComment('Background'); addColorVar('color-bg', background.default); addColorVar('color-success', background.success); addColorVar('color-notice', background.notice); addColorVar('color-warning', background.warning); addColorVar('color-danger', background.danger); addColorVar('color-surprise', background.surprise); addColorVar('color-info', background.info); addNewLine(); } if (block.foreground) { const { foreground } = block; addComment('Foreground'); addColorVar('color-font', foreground.default); addColorVar('color-font-success', foreground.success); addColorVar('color-font-notice', foreground.notice); addColorVar('color-font-warning', foreground.warning); addColorVar('color-font-danger', foreground.danger); addColorVar('color-font-surprise', foreground.surprise); addColorVar('color-font-info', foreground.info); addNewLine(); } if (block.highlight) { const { highlight } = block; addComment('Highlight'); addColorVar('hl', highlight.default); addColorVar('hl-xxs', highlight.xxs); addColorVar('hl-xs', highlight.xs); addColorVar('hl-sm', highlight.sm); addColorVar('hl-md', highlight.md); addColorVar('hl-lg', highlight.lg); addColorVar('hl-xl', highlight.xl); addNewLine(); } return css.replace(/\s+$/, ''); } function wrapStyles(theme: string, selector: string, styles: string) { if (!styles) { return ''; } return [ `[theme="${theme}"] ${selector}, `, `[subtheme="${theme}"] ${selector ? selector + '--sub' : ''} {`, styles, '}', '', '', ].join('\n'); } export async function setTheme(themeName: string) { if (!document) { return; } const head = document.head; const body = document.body; if (!head || !body) { return; } const themes: Array = await getThemes(); // If theme isn't installed for some reason, set to the default if (!themes.find(t => t.theme.name === themeName)) { console.log(`[theme] Theme not found ${themeName}`); themeName = getAppDefaultTheme(); } body.setAttribute('theme', themeName); for (const theme of themes) { let themeCSS = (await generateThemeCSS(theme.theme)) + '\n'; const { name } = theme.theme; const { rawCss } = theme.theme.theme; let s = document.querySelector(`style[data-theme-name="${name}"]`); if (!s) { s = document.createElement('style'); s.setAttribute('data-theme-name', name); head.appendChild(s); } if (typeof rawCss === 'string' && name === themeName) { themeCSS += '\n\n' + rawCss; } s.innerHTML = themeCSS; } } export async function setFont(settings: Object) { if (!document) { return; } const html = document.querySelector('html'); if (!html) { return; } html.style.setProperty('--font-default', settings.fontInterface); html.style.setProperty('--font-monospace', settings.fontMonospace); html.style.setProperty('--font-ligatures', settings.fontVariantLigatures ? 'normal' : 'none'); html.style.setProperty('font-size', `${settings.fontSize}px`); } const _baseTheme = { background: { default: '#fff', success: '#75ba24', notice: '#d8c84d', warning: '#ec8702', danger: '#e15251', surprise: '#6030BF', info: '#20aed9', }, foreground: { default: '#666', success: '#fff', notice: '#fff', warning: '#fff', danger: '#fff', surprise: '#fff', info: '#fff', }, highlight: { default: 'rgba(130, 130, 130, 1)', xxs: 'rgba(130, 130, 130, 0.05)', xs: 'rgba(130, 130, 130, 0.1)', sm: 'rgba(130, 130, 130, 0.25)', md: 'rgba(130, 130, 130, 0.35)', lg: 'rgba(130, 130, 130, 0.5)', xl: 'rgba(130, 130, 130, 0.8)', }, };