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

Suggested implementation of themes refactoring #1340

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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: 1 addition & 0 deletions app/client/app.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* global variables */
@layer grist-base, grist-tokens, grist-theme, grist-custom;
:root {
--color-logo-row: #F9AE41;
--color-logo-col: #2CB0AF;
Expand Down
14 changes: 14 additions & 0 deletions app/client/lib/getOrCreateStyleElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Gets or creates a style element in the head of the document with the given `id`.
*
* Useful for grouping CSS values such as theme custom properties without needing to
* pollute the document with in-line styles.
*/
export function getOrCreateStyleElement(id: string) {
let style = document.head.querySelector(`#${id}`);
if (style) { return style; }
style = document.createElement('style');
style.setAttribute('id', id);
document.head.append(style);
return style;
}
3 changes: 0 additions & 3 deletions app/client/ui/PagePanels.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/**
* Note that it assumes the presence of cssVars.cssRootVars on <body>.
*/
import {makeT} from 'app/client/lib/localization';
import * as commands from 'app/client/components/commands';
import {watchElementForBlur} from 'app/client/lib/FocusLayer';
Expand Down
150 changes: 122 additions & 28 deletions app/client/ui2018/cssVars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
*/
import {urlState} from 'app/client/models/gristUrlState';
import {getTheme, ProductFlavor} from 'app/client/ui/CustomThemes';
import {dom, DomElementMethod, makeTestId, Observable, styled, TestId} from 'grainjs';
import {getOrCreateStyleElement} from 'app/client/lib/getOrCreateStyleElement';
import {DomElementMethod, makeTestId, Observable, styled, TestId} from 'grainjs';
import debounce = require('lodash/debounce');
import values = require('lodash/values');

const VAR_PREFIX = 'grist';

class CustomProp {
constructor(public name: string, public value?: string, public fallback?: string | CustomProp) {
constructor(public name: string, public value?: string | CustomProp, public fallback?: string | CustomProp) {

}

Expand Down Expand Up @@ -77,6 +78,89 @@ export const colors = {

};

/**
* Example of new "design tokens" that are a mix of current `colors`, `vars` and `theme` props.
*
* The css variables defined here take precedence over the ones in `colors` and `vars`, but are
* overriden by variables re-declared in a theme or a custom css file.
*
* The idea is we would not need `colors` and `vars` anymore.
*
* 1) List the `colors` above but rename them to have more abstracted names, ie "lightGreen" becomes "primaryLight".
* Grays are an exception here as I assume they will always be targetted as such, but we could name them differently
* if we want to stick to non-visual names here, like "shadeXX", "neutralXX". Or "secondaryXX", renaming the current
* "secondary" color to "tertiary" or "accent"? I just followed original naming for now.
*
* 2) Whenever possible, design tokens target other design tokens instead of copying color codes, ie "primaryBg"
* directly targets "primaryLight" instead of being "#16B378". Here I use getters to define tokens that target other
* tokens to prevent having to define multiple temporary vars.
*
* 3) Follow the `vars` object idea and add more semantic/global tokens to the list.
* What I have in mind is to move most of `vars` props here, and some of the pretty-much global things listed in `theme`
* (like text colors or panel global things). The endgoal would be to list all colors and tokens globally used in
* Grist here. I guess it might not make sense to list here a few really specific components (for example, code view).
*
* 4) Either have component-specific variables listed in `theme` below consume these designTokens, *or* remove
* them and make it so that components directly consume the designTokens in their own code.
*
* Colors listed here default to Grist light theme colors.
* Contrary to `colors` and `vars`, all tokens here are meant to be overridable by a theme,
* allowing a theme to only override what it wants instead of having to redefine everything.
*/
export const designTokens = {
/* first list hard-coded colors, then other colors consuming them and other non-color tokens */
white: new CustomProp('color-white', '#FFFFFF'),
greyLight: new CustomProp('color-grey-light', '#F7F7F7'),
greyMediumOpaque: new CustomProp('color-grey-medium-opaque', '#E8E8E8'),
greyMedium: new CustomProp('color-grey-medium', 'rgba(217,217,217,0.6)'),
greyDark: new CustomProp('color-grey-dark', '#D9D9D9'),
slate: new CustomProp('color-slate', '#929299'),
darkText: new CustomProp('color-dark-text', '#494949'),
dark: new CustomProp('color-dark', '#262633'),
black: new CustomProp('color-black', '#000000'),

primaryLighter: new CustomProp('color-primary-lighter', '#b1ffe2'),
primaryLight: new CustomProp('color-primary-light', '#16B378'),
primaryDark: new CustomProp('color-primary-dark', '#009058'),
primaryDarker: new CustomProp('color-primary-darker', '#007548'),

secondaryLighter: new CustomProp('color-secondary-lighter', '#87b2f9'),
secondaryLight: new CustomProp('color-secondary-light', '#3B82F6'),

error: new CustomProp('color-error', '#D0021B'),
warningLight: new CustomProp('color-warning-light', '#F9AE41'),
warningDark: new CustomProp('color-warning-dark', '#dd962c'),

cursorInactive: new CustomProp('color-cursor-inactive', '#A2E1C9'),
selection: new CustomProp('color-selection', 'rgba(22,179,120,0.15)'),
selectionOpaque: new CustomProp('color-selection-opaque', '#DCF4EB'),
selectionDarkerOpaque: new CustomProp('color-selection-darker-opaque', '#d6eee5'),
hover: new CustomProp('color-hover', '#bfbfbf'),
backdrop: new CustomProp('color-backdrop', 'rgba(38,38,51,0.9)'),

get warningBg() { return new CustomProp('color-warning-bg', this.warningDark); },

get primaryBg() { return new CustomProp('primary-bg', this.primaryLight); },
get primaryBgHover() { return new CustomProp('primary-bg-hover', this.primaryDark); },
get primaryFg() { return new CustomProp('primary-fg', this.white); },

get controlBg() { return new CustomProp('control-bg', this.white); },
get controlFg() { return new CustomProp('control-fg', this.primaryLight); },
get controlFgHover() { return new CustomProp('primary-fg-hover', this.primaryDark); },
get controlBorderColor() { return new CustomProp('control-border-color', this.primaryLight); },
controlBorderRadius: new CustomProp('border-radius', '4px'),

get cursor() { return new CustomProp('color-cursor', this.primaryLight); },

get mainBg() { return new CustomProp('main-bg', this.white); },
get text() { return new CustomProp('text', this.dark); },
get textLight() { return new CustomProp('text-light', this.slate); },

get panelBg() { return new CustomProp('panel-bg', this.greyLight); },
get panelFg() { return new CustomProp('panel-fg', this.dark); },
get panelBorder() { return new CustomProp('panel-border', this.greyMedium); },
};

export const vars = {
/* Fonts */
fontFamily: new CustomProp('font-family', `-apple-system,BlinkMacSystemFont,Segoe UI,Liberation Sans,
Expand Down Expand Up @@ -177,11 +261,11 @@ export const theme = {
pageBackdrop: new CustomProp('theme-page-backdrop', undefined, 'grey'),

/* Page Panels */
mainPanelBg: new CustomProp('theme-page-panels-main-panel-bg', undefined, 'white'),
leftPanelBg: new CustomProp('theme-page-panels-left-panel-bg', undefined, colors.lightGrey),
rightPanelBg: new CustomProp('theme-page-panels-right-panel-bg', undefined, colors.lightGrey),
topHeaderBg: new CustomProp('theme-page-panels-top-header-bg', undefined, 'white'),
bottomFooterBg: new CustomProp('theme-page-panels-bottom-footer-bg', undefined, 'white'),
mainPanelBg: new CustomProp('theme-page-panels-main-panel-bg', undefined, designTokens.mainBg),
leftPanelBg: new CustomProp('theme-page-panels-left-panel-bg', undefined, designTokens.panelBg),
rightPanelBg: new CustomProp('theme-page-panels-right-panel-bg', undefined, designTokens.panelBg),
topHeaderBg: new CustomProp('theme-page-panels-top-header-bg', undefined, designTokens.mainBg),
bottomFooterBg: new CustomProp('theme-page-panels-bottom-footer-bg', undefined, designTokens.mainBg),
pagePanelsBorder: new CustomProp('theme-page-panels-border', undefined, colors.mediumGrey),
pagePanelsBorderResizing: new CustomProp('theme-page-panels-border-resizing', undefined,
colors.lightGreen),
Expand Down Expand Up @@ -437,20 +521,20 @@ export const theme = {
colors.darkGrey),

/* Right Panel */
rightPanelTabFg: new CustomProp('theme-right-panel-tab-fg', undefined, colors.slate),
rightPanelTabBg: new CustomProp('theme-right-panel-tab-bg', undefined, colors.light),
rightPanelTabIcon: new CustomProp('theme-right-panel-tab-icon', undefined, colors.slate),
rightPanelTabFg: new CustomProp('theme-right-panel-tab-fg', undefined, designTokens.textLight),
rightPanelTabBg: new CustomProp('theme-right-panel-tab-bg', undefined, designTokens.mainBg),
rightPanelTabIcon: new CustomProp('theme-right-panel-tab-icon', undefined, designTokens.textLight),
rightPanelTabIconHover: new CustomProp('theme-right-panel-tab-icon-hover', undefined,
colors.dark),
rightPanelTabBorder: new CustomProp('theme-right-panel-tab-border', undefined, colors.mediumGrey),
rightPanelTabHoverBg: new CustomProp('theme-right-panel-tab-hover-bg', undefined, colors.light),
rightPanelTabHoverFg: new CustomProp('theme-right-panel-tab-hover-fg', undefined, colors.dark),
designTokens.panelFg),
rightPanelTabBorder: new CustomProp('theme-right-panel-tab-border', undefined, designTokens.panelBorder),
rightPanelTabHoverBg: new CustomProp('theme-right-panel-tab-hover-bg', undefined, designTokens.mainBg),
rightPanelTabHoverFg: new CustomProp('theme-right-panel-tab-hover-fg', undefined, designTokens.panelFg),
rightPanelTabSelectedFg: new CustomProp('theme-right-panel-tab-selected-fg', undefined,
colors.dark),
designTokens.panelFg),
rightPanelTabSelectedBg: new CustomProp('theme-right-panel-tab-selected-bg', undefined,
colors.lightGrey),
designTokens.panelBg),
rightPanelTabSelectedIcon: new CustomProp('theme-right-panel-tab-selected-icon', undefined,
colors.lightGreen),
designTokens.primaryBg),
rightPanelTabButtonHoverBg: new CustomProp('theme-right-panel-tab-button-hover-bg',
undefined, colors.darkGreen),
rightPanelSubtabFg: new CustomProp('theme-right-panel-subtab-fg', undefined, colors.lightGreen),
Expand Down Expand Up @@ -922,12 +1006,7 @@ export const theme = {

const cssColors = values(colors).map(v => v.decl()).join('\n');
const cssVars = values(vars).map(v => v.decl()).join('\n');
const cssFontParams = `
font-family: ${vars.fontFamily};
font-size: ${vars.mediumFontSize};
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
`;
const cssTokens = values(designTokens).map(v => v.decl()).join('\n');

// We set box-sizing globally to match bootstrap's setting of border-box, since we are integrating
// into an app which already has it set, and it's impossible to make things look consistently with
Expand Down Expand Up @@ -969,8 +1048,8 @@ const cssFontStyles = `
}
`;

const cssVarsOnly = styled('div', cssColors + cssVars);
const cssBodyVars = styled('div', cssFontParams + cssColors + cssVars + cssBorderBox + cssInputFonts + cssFontStyles);
const cssRootVars = cssColors + cssVars;
const cssReset = cssBorderBox + cssInputFonts + cssFontStyles;

const cssBody = styled('body', `
margin: 0;
Expand All @@ -980,10 +1059,12 @@ const cssBody = styled('body', `
const cssRoot = styled('html', `
height: 100%;
overflow: hidden;
font-family: ${vars.fontFamily};
font-size: ${vars.mediumFontSize};
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
`);

export const cssRootVars = cssBodyVars.className;

// Also make a globally available testId, with a simple "test-" prefix (i.e. in tests, query css
// class ".test-{name}". Ideally, we'd use noTestId() instead in production.
export const testId: TestId = makeTestId('test-');
Expand Down Expand Up @@ -1060,7 +1141,20 @@ export function isScreenResizing(): Observable<boolean> {
* Attaches the global css properties to the document's root to make them available in the page.
*/
export function attachCssRootVars(productFlavor: ProductFlavor, varsOnly: boolean = false) {
dom.update(document.documentElement, varsOnly ? dom.cls(cssVarsOnly.className) : dom.cls(cssRootVars));
/* apply each group of rules and variables in the correct css layer
* see app/client/app.css for layers order */
getOrCreateStyleElement('grist-root-css').textContent = `
@layer grist-base {
:root {
${cssRootVars}
}
${!varsOnly && cssReset}
}
@layer grist-tokens {
:root {
${cssTokens}
}
}`;
document.documentElement.classList.add(cssRoot.className);
document.body.classList.add(cssBody.className);
const customTheme = getTheme(productFlavor);
Expand Down
31 changes: 12 additions & 19 deletions app/client/ui2018/theme.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createPausableObs, PausableObservable } from 'app/client/lib/pausableObs';
import { getStorage } from 'app/client/lib/storage';
import { getOrCreateStyleElement } from 'app/client/lib/getOrCreateStyleElement';
import { urlState } from 'app/client/models/gristUrlState';
import { Theme, ThemeAppearance, ThemeColors, ThemePrefs } from 'app/common/ThemePrefs';
import { getThemeColors } from 'app/common/Themes';
Expand Down Expand Up @@ -119,20 +120,27 @@ function getThemeFromPrefs(themePrefs: ThemePrefs, userAgentPrefersDarkTheme: bo
}

function attachCssThemeVars({appearance, colors: themeColors}: Theme) {
// Prepare the custom properties needed for applying the theme.
const properties = Object.entries(themeColors)
const properties = Object.entries(themeColors.legacyVariables || {})
.map(([name, value]) => `--grist-theme-${name}: ${value};`);

properties.push(...Object.entries(themeColors || {})
.filter(([name]) => name !== 'legacyVariables')
.map(([name, value]) => `--grist-${name}: ${value};`));

// Include properties for styling the scrollbar.
properties.push(...getCssThemeScrollbarProperties(appearance));

// Include properties for picking an appropriate background image.
properties.push(...getCssThemeBackgroundProperties(appearance));

// Apply the properties to the theme style element.
getOrCreateStyleElement('grist-theme').textContent = `:root {
// The 'grist-theme' layer takes precedence over the 'grist-base' and 'grist-tokens'layers where
// default CSS variables are defined.
getOrCreateStyleElement('grist-theme').textContent = `@layer grist-theme {
:root {
${properties.join('\n')}
}`;
}
}`;

// Make the browser aware of the color scheme.
document.documentElement.style.setProperty(`color-scheme`, appearance);
Expand Down Expand Up @@ -174,18 +182,3 @@ function getCssThemeBackgroundProperties(appearance: ThemeAppearance) {
: 'url("img/gplaypattern.png")';
return [`--grist-theme-bg: ${value};`];
}

/**
* Gets or creates a style element in the head of the document with the given `id`.
*
* Useful for grouping CSS values such as theme custom properties without needing to
* pollute the document with in-line styles.
*/
function getOrCreateStyleElement(id: string) {
let style = document.head.querySelector(`#${id}`);
if (style) { return style; }
style = document.createElement('style');
style.setAttribute('id', id);
document.head.append(style);
return style;
}
5 changes: 5 additions & 0 deletions app/common/ThemePrefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export interface Theme {
}

export interface ThemeColors {
legacyVariables?: Partial<LegacyThemeVariables>;
[key: string]: any; /* TODO: improve typings, we should list explicit list of designTokens */
}

interface LegacyThemeVariables {
/* Text */
'text': string;
'text-light': string;
Expand Down
Loading
Loading