Skip to content

Commit 2243fc8

Browse files
committed
(themes) Implementation idea on new design tokens handling
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
1 parent b77bbf7 commit 2243fc8

File tree

6 files changed

+1266
-1165
lines changed

6 files changed

+1266
-1165
lines changed

app/client/app.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* global variables */
2-
@layer grist-base, grist-theme, grist-custom;
2+
@layer grist-base, grist-tokens, grist-theme, grist-custom;
33
:root {
44
--color-logo-row: #F9AE41;
55
--color-logo-col: #2CB0AF;

app/client/ui2018/cssVars.ts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import values = require('lodash/values');
1616
const VAR_PREFIX = 'grist';
1717

1818
class CustomProp {
19-
constructor(public name: string, public value?: string, public fallback?: string | CustomProp) {
19+
constructor(public name: string, public value?: string | CustomProp, public fallback?: string | CustomProp) {
2020

2121
}
2222

@@ -78,6 +78,89 @@ export const colors = {
7878

7979
};
8080

81+
/**
82+
* Example of new "design tokens" that are a mix of current `colors`, `vars` and `theme` props.
83+
*
84+
* The css variables defined here take precedence over the ones in `colors` and `vars`, but are
85+
* overriden by variables re-declared in a theme or a custom css file.
86+
*
87+
* The idea is we would not need `colors` and `vars` anymore.
88+
*
89+
* 1) List the `colors` above but rename them to have more abstracted names, ie "lightGreen" becomes "primaryLight".
90+
* Grays are an exception here as I assume they will always be targetted as such, but we could name them differently
91+
* if we want to stick to non-visual names here, like "shadeXX", "neutralXX". Or "secondaryXX", renaming the current
92+
* "secondary" color to "tertiary" or "accent"? I just followed original naming for now.
93+
*
94+
* 2) Whenever possible, design tokens target other design tokens instead of copying color codes, ie "primaryBg"
95+
* directly targets "primaryLight" instead of being "#16B378". Here I use getters to define tokens that target other
96+
* tokens to prevent having to define multiple temporary vars.
97+
*
98+
* 3) Follow the `vars` object idea and add more semantic/global tokens to the list.
99+
* What I have in mind is to move most of `vars` props here, and some of the pretty-much global things listed in `theme`
100+
* (like text colors or panel global things). The endgoal would be to list all colors and tokens globally used in
101+
* Grist here. I guess it might not make sense to list here a few really specific components (for example, code view).
102+
*
103+
* 4) Either have component-specific variables listed in `theme` below consume these designTokens, *or* remove
104+
* them and make it so that components directly consume the designTokens in their own code.
105+
*
106+
* Colors listed here default to Grist light theme colors.
107+
* Contrary to `colors` and `vars`, all tokens here are meant to be overridable by a theme,
108+
* allowing a theme to only override what it wants instead of having to redefine everything.
109+
*/
110+
export const designTokens = {
111+
/* first list hard-coded colors, then other colors consuming them and other non-color tokens */
112+
white: new CustomProp('color-white', '#FFFFFF'),
113+
greyLight: new CustomProp('color-grey-light', '#F7F7F7'),
114+
greyMediumOpaque: new CustomProp('color-grey-medium-opaque', '#E8E8E8'),
115+
greyMedium: new CustomProp('color-grey-medium', 'rgba(217,217,217,0.6)'),
116+
greyDark: new CustomProp('color-grey-dark', '#D9D9D9'),
117+
slate: new CustomProp('color-slate', '#929299'),
118+
darkText: new CustomProp('color-dark-text', '#494949'),
119+
dark: new CustomProp('color-dark', '#262633'),
120+
black: new CustomProp('color-black', '#000000'),
121+
122+
primaryLighter: new CustomProp('color-primary-lighter', '#b1ffe2'),
123+
primaryLight: new CustomProp('color-primary-light', '#16B378'),
124+
primaryDark: new CustomProp('color-primary-dark', '#009058'),
125+
primaryDarker: new CustomProp('color-primary-darker', '#007548'),
126+
127+
secondaryLighter: new CustomProp('color-secondary-lighter', '#87b2f9'),
128+
secondaryLight: new CustomProp('color-secondary-light', '#3B82F6'),
129+
130+
error: new CustomProp('color-error', '#D0021B'),
131+
warningLight: new CustomProp('color-warning-light', '#F9AE41'),
132+
warningDark: new CustomProp('color-warning-dark', '#dd962c'),
133+
134+
cursorInactive: new CustomProp('color-cursor-inactive', '#A2E1C9'),
135+
selection: new CustomProp('color-selection', 'rgba(22,179,120,0.15)'),
136+
selectionOpaque: new CustomProp('color-selection-opaque', '#DCF4EB'),
137+
selectionDarkerOpaque: new CustomProp('color-selection-darker-opaque', '#d6eee5'),
138+
hover: new CustomProp('color-hover', '#bfbfbf'),
139+
backdrop: new CustomProp('color-backdrop', 'rgba(38,38,51,0.9)'),
140+
141+
get warningBg() { return new CustomProp('color-warning-bg', this.warningDark); },
142+
143+
get primaryBg() { return new CustomProp('primary-bg', this.primaryLight); },
144+
get primaryBgHover() { return new CustomProp('primary-bg-hover', this.primaryDark); },
145+
get primaryFg() { return new CustomProp('primary-fg', this.white); },
146+
147+
get controlBg() { return new CustomProp('control-bg', this.white); },
148+
get controlFg() { return new CustomProp('control-fg', this.primaryLight); },
149+
get controlFgHover() { return new CustomProp('primary-fg-hover', this.primaryDark); },
150+
get controlBorderColor() { return new CustomProp('control-border-color', this.primaryLight); },
151+
controlBorderRadius: new CustomProp('border-radius', '4px'),
152+
153+
get cursor() { return new CustomProp('color-cursor', this.primaryLight); },
154+
155+
get mainBg() { return new CustomProp('main-bg', this.white); },
156+
get text() { return new CustomProp('text', this.dark); },
157+
get textLight() { return new CustomProp('text-light', this.slate); },
158+
159+
get panelBg() { return new CustomProp('panel-bg', this.greyLight); },
160+
get panelFg() { return new CustomProp('panel-fg', this.dark); },
161+
get panelBorder() { return new CustomProp('panel-border', this.greyMedium); },
162+
};
163+
81164
export const vars = {
82165
/* Fonts */
83166
fontFamily: new CustomProp('font-family', `-apple-system,BlinkMacSystemFont,Segoe UI,Liberation Sans,
@@ -923,6 +1006,7 @@ export const theme = {
9231006

9241007
const cssColors = values(colors).map(v => v.decl()).join('\n');
9251008
const cssVars = values(vars).map(v => v.decl()).join('\n');
1009+
const cssTokens = values(designTokens).map(v => v.decl()).join('\n');
9261010

9271011
// We set box-sizing globally to match bootstrap's setting of border-box, since we are integrating
9281012
// into an app which already has it set, and it's impossible to make things look consistently with
@@ -1065,6 +1149,11 @@ export function attachCssRootVars(productFlavor: ProductFlavor, varsOnly: boolea
10651149
${cssRootVars}
10661150
}
10671151
${!varsOnly && cssReset}
1152+
}
1153+
@layer grist-tokens {
1154+
:root {
1155+
${cssTokens}
1156+
}
10681157
}`;
10691158
document.documentElement.classList.add(cssRoot.className);
10701159
document.body.classList.add(cssBody.className);

app/client/ui2018/theme.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,18 +120,21 @@ function getThemeFromPrefs(themePrefs: ThemePrefs, userAgentPrefersDarkTheme: bo
120120
}
121121

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

126+
properties.push(...Object.entries(themeColors || {})
127+
.filter(([name]) => name !== 'legacyVariables')
128+
.map(([name, value]) => `--grist-${name}: ${value};`));
129+
127130
// Include properties for styling the scrollbar.
128131
properties.push(...getCssThemeScrollbarProperties(appearance));
129132

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

133136
// Apply the properties to the theme style element.
134-
// The 'grist-theme' layer takes precedence over the 'grist-base' layer where
137+
// The 'grist-theme' layer takes precedence over the 'grist-base' and 'grist-tokens'layers where
135138
// default CSS variables are defined.
136139
getOrCreateStyleElement('grist-theme').textContent = `@layer grist-theme {
137140
:root {

app/common/ThemePrefs.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export interface Theme {
2222
}
2323

2424
export interface ThemeColors {
25+
legacyVariables?: Partial<LegacyThemeVariables>;
26+
[key: string]: any; /* TODO: improve typings, we should list explicit list of designTokens */
27+
}
28+
29+
interface LegacyThemeVariables {
2530
/* Text */
2631
'text': string;
2732
'text-light': string;

0 commit comments

Comments
 (0)