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 4 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-theme, grist-custom;
:root {
--color-logo-row: #F9AE41;
--color-logo-col: #2CB0AF;
Expand Down
12 changes: 6 additions & 6 deletions app/client/components/ChartView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,19 +395,19 @@ export class ChartView extends Disposable {
private _getPlotlyTheme(): Partial<Layout> {
const {colors} = gristThemeObs().get();
return {
paper_bgcolor: colors['chart-bg'],
plot_bgcolor: colors['chart-bg'],
paper_bgcolor: colors.legacyVariables.chartBg.toString(),
plot_bgcolor: colors.legacyVariables.chartBg.toString(),
xaxis: {
color: colors['chart-x-axis'],
color: colors.legacyVariables.chartXAxis.toString(),
},
yaxis: {
color: colors['chart-y-axis'],
color: colors.legacyVariables.chartYAxis.toString(),
},
font: {
color: colors['chart-fg'],
color: colors.legacyVariables.chartFg.toString(),
},
legend: {
bgcolor: colors['chart-legend-bg'],
bgcolor: colors.legacyVariables.chartLegendBg.toString(),
},
};
}
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: 1 addition & 2 deletions app/client/models/AppModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {LocalPlugin} from 'app/common/plugin';
import {DismissedPopup, DismissedReminder, UserPrefs} from 'app/common/Prefs';
import {isOwner, isOwnerOrEditor} from 'app/common/roles';
import {getTagManagerScript} from 'app/common/tagManager';
import {getDefaultThemePrefs, ThemePrefs, ThemePrefsChecker} from 'app/common/ThemePrefs';
import {getDefaultThemePrefs, ThemePrefs} from 'app/common/ThemePrefs';
import {getGristConfig} from 'app/common/urlUtils';
import {ExtendedUser} from 'app/common/UserAPI';
import {getOrgName, isTemplatesOrg, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
Expand Down Expand Up @@ -305,7 +305,6 @@ export class AppModelImpl extends Disposable implements AppModel {
public readonly userPrefsObs = getUserPrefsObs(this);
public readonly themePrefs = getUserPrefObs(this.userPrefsObs, 'theme', {
defaultValue: getDefaultThemePrefs(),
checker: ThemePrefsChecker,
}) as Observable<ThemePrefs>;

public readonly dismissedPopups = getUserPrefObs(this.userPrefsObs, 'dismissedPopups',
Expand Down
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
957 changes: 61 additions & 896 deletions app/client/ui2018/cssVars.ts

Large diffs are not rendered by default.

79 changes: 44 additions & 35 deletions app/client/ui2018/theme.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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';
import { legacyVariables, Theme, ThemeAppearance, ThemePrefs, ThemeTokens, tokens } from 'app/common/ThemePrefs';
import { getThemeTokens } from 'app/common/Themes';
import { getGristConfig } from 'app/common/urlUtils';
import { Computed, Observable } from 'grainjs';
import isEqual from 'lodash/isEqual';

const DEFAULT_LIGHT_THEME: Theme = {appearance: 'light', colors: getThemeColors('GristLight')};
const DEFAULT_DARK_THEME: Theme = {appearance: 'dark', colors: getThemeColors('GristDark')};
const DEFAULT_LIGHT_THEME: Theme = {appearance: 'light', colors: getThemeTokens('GristLight')};
const DEFAULT_DARK_THEME: Theme = {appearance: 'dark', colors: getThemeTokens('GristDark')};

/**
* A singleton observable for the current user's Grist theme preferences.
Expand Down Expand Up @@ -66,11 +67,11 @@ export function gristThemeObs() {
/**
* Attaches the current theme's CSS variables to the document, and
* re-attaches them whenever the theme changes.
*
* When custom CSS is enabled (and theme selection is then unavailable in the UI),
* default light theme variables are attached.
*/
export function attachTheme() {
// Custom CSS is incompatible with custom themes.
if (getGristConfig().enableCustomCss) { return; }

// Attach the current theme's variables to the DOM.
attachCssThemeVars(gristThemeObs().get());

Expand Down Expand Up @@ -103,25 +104,44 @@ function getThemeFromPrefs(themePrefs: ThemePrefs, userAgentPrefersDarkTheme: bo
appearance = userAgentPrefersDarkTheme ? 'dark' : 'light';
}

let nameOrColors = themePrefs.colors[appearance];
let nameOrTokens = themePrefs.colors[appearance];
if (urlParams?.themeName) {
nameOrColors = urlParams?.themeName;
nameOrTokens = urlParams?.themeName;
}

let colors: ThemeColors;
if (typeof nameOrColors === 'string') {
colors = getThemeColors(nameOrColors);
let themeTokens: ThemeTokens;
if (typeof nameOrTokens === 'string') {
themeTokens = getThemeTokens(nameOrTokens);
} else {
colors = nameOrColors;
themeTokens = nameOrTokens;
}

return {appearance, colors};
return {appearance, colors: themeTokens};
}

function attachCssThemeVars({appearance, colors: themeColors}: Theme) {
// Prepare the custom properties needed for applying the theme.
const properties = Object.entries(themeColors)
.map(([name, value]) => `--grist-theme-${name}: ${value};`);
function attachCssThemeVars({appearance, colors: themeTokens}: Theme) {
const properties = Object.entries(themeTokens || {})
.filter(([name]) => name !== 'legacyVariables')
.map(([tokenName, value]) => {
if (tokenName in tokens) {
const cssProp = tokens[tokenName as keyof typeof tokens];
cssProp.value = value;
return cssProp.decl();
}
return undefined;
})
.filter((prop): prop is string => prop !== undefined);

properties.push(...Object.entries(themeTokens.legacyVariables || {})
.map(([tokenName, value]) => {
if (tokenName in legacyVariables) {
const cssProp = legacyVariables[tokenName as keyof typeof legacyVariables];
cssProp.value = value;
return cssProp.decl();
}
return undefined;
})
.filter((prop): prop is string => prop !== undefined));

// Include properties for styling the scrollbar.
properties.push(...getCssThemeScrollbarProperties(appearance));
Expand All @@ -130,9 +150,13 @@ function attachCssThemeVars({appearance, colors: themeColors}: Theme) {
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' layer 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 +198,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;
}
21 changes: 21 additions & 0 deletions app/common/CssCustomProp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const VAR_PREFIX = 'grist';

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

}

public decl(): string | undefined {
if (this.value === undefined) { return undefined; }

return `--${VAR_PREFIX}-${this.name}: ${this.value};`;
}

public toString(): string {
let value = `--${VAR_PREFIX}-${this.name}`;
if (this.fallback) {
value += `, ${this.fallback}`;
}
return `var(${value})`;
}
}
Loading