Skip to content

Commit

Permalink
(themes) Implementation idea on new design tokens handling
Browse files Browse the repository at this point in the history
Until now themes could only describe each component variable. This made
creating a theme difficult: we had to override all component variables
(hundreds of them) if we wanted to change one color shade used globally.

Here is an idea of a cssVars.designTokens prop that is a combination of
current `colors`, `vars` and `theme` props:

- all design tokens are overridable by a theme
- colors are now named in a more abstract way so that it makes sense for
a theme changing the primary color

Everything is explained in details in the cssVars.designTokens comments
  • Loading branch information
manuhabitela committed Dec 12, 2024
1 parent b77bbf7 commit 2243fc8
Show file tree
Hide file tree
Showing 6 changed files with 1,266 additions and 1,165 deletions.
2 changes: 1 addition & 1 deletion app/client/app.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* global variables */
@layer grist-base, grist-theme, grist-custom;
@layer grist-base, grist-tokens, grist-theme, grist-custom;
:root {
--color-logo-row: #F9AE41;
--color-logo-col: #2CB0AF;
Expand Down
91 changes: 90 additions & 1 deletion app/client/ui2018/cssVars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ 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 @@ -78,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 @@ -923,6 +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 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 @@ -1065,6 +1149,11 @@ export function attachCssRootVars(productFlavor: ProductFlavor, varsOnly: boolea
${cssRootVars}
}
${!varsOnly && cssReset}
}
@layer grist-tokens {
:root {
${cssTokens}
}
}`;
document.documentElement.classList.add(cssRoot.className);
document.body.classList.add(cssBody.className);
Expand Down
9 changes: 6 additions & 3 deletions app/client/ui2018/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,18 +120,21 @@ 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.
// The 'grist-theme' layer takes precedence over the 'grist-base' layer where
// 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 {
Expand Down
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

0 comments on commit 2243fc8

Please sign in to comment.