diff --git a/app/client/app.css b/app/client/app.css index b707a2583b..8b993f80da 100644 --- a/app/client/app.css +++ b/app/client/app.css @@ -1,4 +1,5 @@ /* global variables */ +@layer grist-base, grist-theme, grist-custom; :root { --color-logo-row: #F9AE41; --color-logo-col: #2CB0AF; diff --git a/app/client/lib/getOrCreateStyleElement.ts b/app/client/lib/getOrCreateStyleElement.ts new file mode 100644 index 0000000000..f29046726a --- /dev/null +++ b/app/client/lib/getOrCreateStyleElement.ts @@ -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; +} diff --git a/app/client/ui/PagePanels.ts b/app/client/ui/PagePanels.ts index 6a95877954..ba09819a2d 100644 --- a/app/client/ui/PagePanels.ts +++ b/app/client/ui/PagePanels.ts @@ -1,6 +1,3 @@ -/** - * Note that it assumes the presence of cssVars.cssRootVars on . - */ import {makeT} from 'app/client/lib/localization'; import * as commands from 'app/client/components/commands'; import {watchElementForBlur} from 'app/client/lib/FocusLayer'; diff --git a/app/client/ui2018/cssVars.ts b/app/client/ui2018/cssVars.ts index 88aaa8cc99..fa990f0011 100644 --- a/app/client/ui2018/cssVars.ts +++ b/app/client/ui2018/cssVars.ts @@ -8,7 +8,8 @@ */ 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'); @@ -922,12 +923,6 @@ 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; -`; // 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 @@ -969,8 +964,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; @@ -980,10 +975,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-'); @@ -1060,7 +1057,15 @@ export function isScreenResizing(): Observable { * 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} +}`; document.documentElement.classList.add(cssRoot.className); document.body.classList.add(cssBody.className); const customTheme = getTheme(productFlavor); diff --git a/app/client/ui2018/theme.ts b/app/client/ui2018/theme.ts index 642732adae..019a368d29 100644 --- a/app/client/ui2018/theme.ts +++ b/app/client/ui2018/theme.ts @@ -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'; @@ -130,9 +131,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); @@ -174,18 +179,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; -} diff --git a/static/custom.css b/static/custom.css index fc5f332c48..a06b5b83a9 100644 --- a/static/custom.css +++ b/static/custom.css @@ -1,42 +1,44 @@ -:root { - /* logo */ - --icon-GristLogo: url("ui-icons/Logo/GristLogo.svg") !important; - --grist-logo-bg: #040404 !important; - --grist-logo-size: 22px 22px !important; +@layer grist-custom { + :root { + /* logo */ + --icon-GristLogo: url("ui-icons/Logo/GristLogo.svg") !important; + --grist-logo-bg: #040404 !important; + --grist-logo-size: 22px 22px !important; - /* colors */ - --grist-color-light-grey: #F7F7F7 !important; - --grist-color-medium-grey: rgba(217,217,217,0.6) !important; - --grist-color-medium-grey-opaque: #E8E8E8 !important; - --grist-color-dark-grey: #D9D9D9 !important; - --grist-color-light: #FFFFFF !important; - --grist-color-dark: #262633 !important; - --grist-color-dark-bg: #262633 !important; - --grist-color-slate: #929299 !important; - --grist-color-light-green: #16B378 !important; - --grist-color-dark-green: #009058 !important; - --grist-color-darker-green: #007548 !important; - --grist-color-lighter-green: #b1ffe2 !important; - --grist-color-lighter-blue: #87b2f9 !important; - --grist-color-light-blue: #3B82F6 !important; - --grist-color-cursor: #16B378 !important; - --grist-color-selection: rgba(22,179,120,0.15) !important; - --grist-color-selection-opaque: #DCF4EB !important; - --grist-color-selection-darker-opaque: #d6eee5 !important; - --grist-color-inactive-cursor: #A2E1C9 !important; - --grist-color-hover: #bfbfbf !important; - --grist-color-error: #D0021B !important; - --grist-color-warning: #F9AE41 !important; - --grist-color-warning-bg: #dd962c !important; - --grist-color-backdrop: rgba(38,38,51,0.9) !important; - --grist-label-text-bg: #FFFFFF !important; - --grist-label-active-bg: #F0F0F0 !important; - --grist-primary-fg: #16B378 !important; - --grist-primary-fg-hover: #009058 !important; - --grist-primary-bg: #ffffff !important; - --grist-control-bg: #ffffff !important; - --grist-control-fg: #16B378 !important; - --grist-primary-fg-hover: #009058 !important; - --grist-control-border: 1px solid #11B683 !important; - --grist-toast-bg: #040404 !important; + /* colors */ + --grist-color-light-grey: #F7F7F7 !important; + --grist-color-medium-grey: rgba(217,217,217,0.6) !important; + --grist-color-medium-grey-opaque: #E8E8E8 !important; + --grist-color-dark-grey: #D9D9D9 !important; + --grist-color-light: #FFFFFF !important; + --grist-color-dark: #262633 !important; + --grist-color-dark-bg: #262633 !important; + --grist-color-slate: #929299 !important; + --grist-color-light-green: #16B378 !important; + --grist-color-dark-green: #009058 !important; + --grist-color-darker-green: #007548 !important; + --grist-color-lighter-green: #b1ffe2 !important; + --grist-color-lighter-blue: #87b2f9 !important; + --grist-color-light-blue: #3B82F6 !important; + --grist-color-cursor: #16B378 !important; + --grist-color-selection: rgba(22,179,120,0.15) !important; + --grist-color-selection-opaque: #DCF4EB !important; + --grist-color-selection-darker-opaque: #d6eee5 !important; + --grist-color-inactive-cursor: #A2E1C9 !important; + --grist-color-hover: #bfbfbf !important; + --grist-color-error: #D0021B !important; + --grist-color-warning: #F9AE41 !important; + --grist-color-warning-bg: #dd962c !important; + --grist-color-backdrop: rgba(38,38,51,0.9) !important; + --grist-label-text-bg: #FFFFFF !important; + --grist-label-active-bg: #F0F0F0 !important; + --grist-primary-fg: #16B378 !important; + --grist-primary-fg-hover: #009058 !important; + --grist-primary-bg: #ffffff !important; + --grist-control-bg: #ffffff !important; + --grist-control-fg: #16B378 !important; + --grist-primary-fg-hover: #009058 !important; + --grist-control-border: 1px solid #11B683 !important; + --grist-toast-bg: #040404 !important; + } }