(core) Add dark mode to user preferences

Summary:
Adds initial implementation of dark mode. Preferences for dark mode are
available on the account settings page. Dark mode is currently a beta feature
as there are still some small bugs to squash and a few remaining UI elements
to style.

Test Plan: Browser tests.

Reviewers: jarek

Reviewed By: jarek

Subscribers: paulfitz, jarek

Differential Revision: https://phab.getgrist.com/D3587
This commit is contained in:
George Gevoian
2022-09-05 18:51:57 -07:00
parent d7b3fb972c
commit ec157dc469
122 changed files with 3616 additions and 1075 deletions

View File

@@ -1,6 +1,6 @@
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
import {isLight, swatches} from 'app/client/ui2018/ColorPalette';
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
import {colors, testId, theme, vars} from 'app/client/ui2018/cssVars';
import {textInput} from 'app/client/ui2018/editableLabel';
import {IconName} from 'app/client/ui2018/IconList';
import {icon} from 'app/client/ui2018/icons';
@@ -348,7 +348,8 @@ const cssFontOption = styled('div', `
display: grid;
place-items: center;
flex-grow: 1;
background: white;
background: ${colors.light};
--icon-color: ${colors.dark};
height: 24px;
cursor: pointer;
&:hover:not(&-selected) {
@@ -418,12 +419,13 @@ const cssContent = styled('div', `
`);
const cssHexBox = styled(textInput, `
border: 1px solid ${colors.darkGrey};
border: 1px solid ${theme.inputBorder};
border-left: none;
font-size: ${vars.smallFontSize};
display: flex;
align-items: center;
color: ${colors.slate};
color: ${theme.lightText};
background-color: ${theme.inputBg};
width: 56px;
outline: none;
padding: 0 3px;

View File

@@ -6,35 +6,27 @@
* Workspace is a clickable link and document and page names are editable labels.
*/
import { urlState } from 'app/client/models/gristUrlState';
import { colors, cssHideForNarrowScreen, mediaNotSmall, testId } from 'app/client/ui2018/cssVars';
import { cssHideForNarrowScreen, mediaNotSmall, testId, theme } from 'app/client/ui2018/cssVars';
import { editableLabel } from 'app/client/ui2018/editableLabel';
import { icon } from 'app/client/ui2018/icons';
import { cssLink } from 'app/client/ui2018/links';
import { UserOverride } from 'app/common/DocListAPI';
import { userOverrideParams } from 'app/common/gristUrls';
import { BindableValue, dom, Observable, styled } from 'grainjs';
import { tooltip } from 'popweasel';
export const cssBreadcrumbs = styled('div', `
color: ${colors.slate};
color: ${theme.lightText};
white-space: nowrap;
cursor: default;
`);
export const cssBreadcrumbsLink = styled('a', `
color: ${colors.lightGreen};
text-decoration: none;
&:hover {
text-decoration: underline;
}
`);
export const separator = styled('span', `
padding: 0 2px;
`);
const cssIcon = styled(icon, `
background-color: ${colors.lightGreen};
background-color: ${theme.accentIcon};
margin-top: -2px;
`);
@@ -43,7 +35,7 @@ const cssPublicIcon = styled(cssIcon, `
margin-top: -4px;
`);
const cssWorkspaceName = styled(cssBreadcrumbsLink, `
const cssWorkspaceName = styled(cssLink, `
margin-left: 8px;
`);
@@ -54,7 +46,7 @@ const cssWorkspaceNarrowScreen = styled(icon, `
margin-bottom: 4px;
margin-left: -7px;
margin-right: 8px;
background-color: ${colors.slate};
background-color: ${theme.lightText};
cursor: pointer;
@media ${mediaNotSmall} {
& {
@@ -65,21 +57,21 @@ const cssWorkspaceNarrowScreen = styled(icon, `
const cssEditableName = styled('input', `
&:hover, &:focus {
color: ${colors.dark};
color: ${theme.text};
}
`);
const cssTag = styled('span', `
background-color: ${colors.slate};
color: white;
background-color: ${theme.breadcrumbsTagBg};
color: ${theme.breadcrumbsTagFg};
border-radius: 3px;
padding: 0 4px;
margin-left: 4px;
`);
const cssAlertTag = styled(cssTag, `
background-color: ${colors.error};
--icon-color: white;
background-color: ${theme.breadcrumbsTagAlertBg};
--icon-color: ${theme.breadcrumbsTagFg};
a {
cursor: pointer;
}

View File

@@ -1,4 +1,4 @@
import {colors, testId, vars} from 'app/client/ui2018/cssVars';
import {colors, testId, theme, vars} from 'app/client/ui2018/cssVars';
import {IconName} from 'app/client/ui2018/IconList';
import {icon} from 'app/client/ui2018/icons';
import {isColorDark} from 'app/common/gutil';
@@ -142,7 +142,7 @@ export const cssButtonSelect = styled('div', `
display: flex;
/* Vars */
color: ${colors.dark};
color: ${theme.text};
flex: 1 1 0;
`);
@@ -165,8 +165,9 @@ const cssSelectorBtn = styled('div', `
white-space: nowrap;
padding: 4px 10px;
border: 1px solid ${colors.darkGrey};
--icon-color: ${colors.slate};
background-color: ${theme.buttonGroupBg};
border: 1px solid ${theme.buttonGroupBorder};
--icon-color: ${theme.buttonGroupIcon};
margin-left: -1px;
@@ -184,15 +185,15 @@ const cssSelectorBtn = styled('div', `
}
&:hover:not(&-selected) {
border: 1px solid ${colors.hover};
border: 1px solid ${theme.buttonGroupBorderHover};
z-index: 5; /* Update z-index so selected borders take precedent */
}
&-selected {
color: ${colors.light};
--icon-color: ${colors.light};
border: 1px solid ${colors.dark};
background-color: ${colors.dark};
color: ${theme.buttonGroupSelectedFg};
--icon-color: ${theme.buttonGroupSelectedFg};
border: 1px solid ${theme.buttonGroupSelectedBorder};
background-color: ${theme.buttonGroupSelectedBg};
z-index: 10; /* Update z-index so selected borders take precedent */
}
@@ -202,18 +203,18 @@ const cssSelectorBtn = styled('div', `
border-radius: ${vars.controlBorderRadius};
margin-left: 0px;
padding: 8px;
color: ${colors.slate};
--icon-color: ${colors.slate};
color: ${theme.buttonGroupLightFg};
--icon-color: ${theme.buttonGroupLightFg};
}
.${cssButtonSelect.className}-light > &-selected {
border: none;
color: ${colors.lightGreen};
--icon-color: ${colors.lightGreen};
color: ${theme.buttonGroupLightSelectedFg};
--icon-color: ${theme.buttonGroupLightSelectedFg};
background-color: initial;
}
.${cssButtonSelect.className}-light > &:hover {
border: none;
background-color: ${colors.mediumGrey};
background-color: ${theme.hover};
}
`);

View File

@@ -12,7 +12,7 @@
* `primaryButton('Primary button', dom.prop('disabled', true))`
*/
import { colors, vars } from 'app/client/ui2018/cssVars';
import { theme, vars } from 'app/client/ui2018/cssVars';
import { tbind } from 'app/common/tbind';
import { dom, DomElementArg, styled } from 'grainjs';
@@ -29,11 +29,12 @@ export const cssButton = styled('button', `
padding: 4px 8px;
background-color: transparent;
color: ${vars.controlFg};
--icon-color: ${vars.controlFg};
color: ${theme.controlFg};
--icon-color: ${theme.controlFg};
border: ${vars.controlBorder};
border-radius: ${vars.controlBorderRadius};
border-color: ${theme.controlBorder};
cursor: pointer;
@@ -44,29 +45,29 @@ export const cssButton = styled('button', `
}
&-primary {
background-color: ${vars.primaryBg};
color: ${vars.primaryFg};
--icon-color: ${vars.primaryFg};
border-color: ${vars.primaryBg};
background-color: ${theme.controlPrimaryBg};
color: ${theme.controlPrimaryFg};
--icon-color: ${theme.controlPrimaryFg};
border-color: ${theme.controlPrimaryBg};
}
&:hover {
color: ${vars.controlFgHover};
--icon-color: ${vars.controlFgHover};
border-color: ${vars.controlFgHover};
color: ${theme.controlHoverFg};
--icon-color: ${theme.controlHoverFg};
border-color: ${theme.controlHoverFg};
}
&-primary:hover {
color: ${vars.primaryFg};
--icon-color: ${vars.primaryFg};
background-color: ${vars.primaryBgHover};
border-color: ${vars.primaryBgHover};
color: ${theme.controlPrimaryFg};
--icon-color: ${theme.controlPrimaryFg};
background-color: ${theme.controlPrimaryHoverBg};
border-color: ${theme.controlPrimaryHoverBg};
}
&:disabled {
cursor: not-allowed;
color: ${colors.light};
--icon-color: ${colors.light};
background-color: ${colors.slate};
border-color: ${colors.slate};
color: ${theme.controlDisabledFg};
--icon-color: ${theme.controlDisabledFg};
background-color: ${theme.controlDisabledBg};
border-color: ${theme.controlDisabledBg};
}
`);
@@ -107,7 +108,7 @@ export const textButton = styled(cssButton, `
padding: 0px;
background-color: inherit !important;
&:disabled {
color: ${colors.inactiveCursor};
color: ${theme.controlPrimaryDisabled};
}
`);

View File

@@ -15,7 +15,7 @@
* labeledSquareCheckbox(observable(false), 'Include other values', dom.prop('disabled', true)),
*/
import { colors } from 'app/client/ui2018/cssVars';
import { theme } from 'app/client/ui2018/cssVars';
import { Computed, dom, DomArg, styled } from 'grainjs';
import { Observable } from 'grainjs';
@@ -28,9 +28,9 @@ export const cssLabel = styled('label', `
outline: none;
user-select: none;
--color: ${colors.darkGrey};
--color: ${theme.checkboxBorder};
&:hover {
--color: ${colors.hover};
--color: ${theme.checkboxBorderHover};
}
`);
@@ -53,20 +53,14 @@ export const cssCheckboxSquare = styled('input', `
--radius: 3px;
&:checked:enabled, &:indeterminate:enabled {
--color: ${colors.lightGreen};
--color: ${theme.controlPrimaryBg};
}
&:disabled {
--color: ${colors.darkGrey};
--color: ${theme.checkboxDisabledBg};
cursor: not-allowed;
}
.${cssLabel.className}:hover > &:checked:enabled,
.${cssLabel.className}:hover > &:indeterminate:enabled, {
--color: ${colors.darkGreen};
}
&::before, &::after {
content: '';
@@ -86,6 +80,14 @@ export const cssCheckboxSquare = styled('input', `
background-color: var(--color);
}
&:not(:checked):indeterminate::after {
-webkit-mask-image: var(--icon-Minus);
}
&:not(:disabled)::after {
background-color: ${theme.checkboxBg};
}
&:checked::after, &:indeterminate::after {
content: '';
position: absolute;
@@ -95,15 +97,7 @@ export const cssCheckboxSquare = styled('input', `
-webkit-mask-size: contain;
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
background-color: ${colors.light};
}
&:not(:checked):indeterminate::after {
-webkit-mask-image: var(--icon-Minus);
}
&:not(:disabled)::after {
background-color: ${colors.light};
background-color: ${theme.controlPrimaryFg};
}
`);
@@ -113,7 +107,7 @@ export const cssCheckboxCircle = styled(cssCheckboxSquare, `
export const cssLabelText = styled('span', `
margin-left: 8px;
color: ${colors.dark};
color: ${theme.text};
font-weight: initial; /* negate bootstrap */
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -8,6 +8,7 @@
*/
import {urlState} from 'app/client/models/gristUrlState';
import {getTheme, ProductFlavor} from 'app/client/ui/CustomThemes';
import {Theme, ThemeAppearance} from 'app/common/ThemePrefs';
import {dom, makeTestId, Observable, styled, TestId} from 'grainjs';
import debounce = require('lodash/debounce');
import values = require('lodash/values');
@@ -15,17 +16,32 @@ import values = require('lodash/values');
const VAR_PREFIX = 'grist';
class CustomProp {
constructor(public name: string, public value: string) { }
constructor(public name: string, public value?: string, public fallback?: string | CustomProp) {
}
public decl(): string | undefined {
if (this.value === undefined) { return undefined; }
public decl() {
return `--${VAR_PREFIX}-${this.name}: ${this.value};`;
}
public toString() {
return `var(--${VAR_PREFIX}-${this.name})`;
public toString(): string {
let value = `--${VAR_PREFIX}-${this.name}`;
if (this.fallback) {
value += `, ${this.fallback}`;
}
return `var(${value})`;
}
}
/**
* Theme-agnostic color properties.
*
* These are appropriate for UI elements whose color should not change based on the active
* theme. Generally, you should instead use the properties defined in `theme`, which will change
* based on the active theme.
*/
export const colors = {
lightGrey: new CustomProp('color-light-grey', '#F7F7F7'),
mediumGrey: new CustomProp('color-medium-grey', 'rgba(217,217,217,0.6)'),
@@ -46,7 +62,7 @@ export const colors = {
lightBlue: new CustomProp('color-light-blue', '#3B82F6'),
orange: new CustomProp('color-orange', '#F9AE41'),
cursor: new CustomProp('color-cursor', '#16B378'), // cursor is lightGreen
cursor: new CustomProp('color-cursor', '#16B378'),
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'),
@@ -117,6 +133,512 @@ export const vars = {
toastBg: new CustomProp('toast-bg', '#040404'),
};
/**
* Theme-related color properties.
*
* Unlike `colors`, these properties don't define any values as they aren't known ahead of time.
* Instead, when the application loads, CSS variables mapped to these properties are attached to
* the document based on the user's theme preferences.
*
* In the case that CSS variables aren't attached to the document, their fallback values will be
* used. This ensures that styles are still applied even when there's trouble fetching preferences,
* and also serves as a method of maintaining backwards compatibility with custom CSS rules that
* use legacy variable names (prefixed with `grist-color-`).
*/
export const theme = {
/* Text */
text: new CustomProp('theme-text', undefined, colors.dark),
lightText: new CustomProp('theme-text-light', undefined, colors.slate),
darkText: new CustomProp('theme-text-dark', undefined, 'black'),
errorText: new CustomProp('theme-text-error', undefined, colors.error),
dangerText: new CustomProp('theme-text-danger', undefined, '#FFA500'),
disabledText: new CustomProp('theme-text-disabled', undefined, colors.slate),
/* Page */
pageBg: new CustomProp('theme-page-bg', undefined, colors.lightGrey),
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'),
pagePanelsBorder: new CustomProp('theme-page-panels-border', undefined, colors.mediumGrey),
pagePanelsBorderResizing: new CustomProp('theme-page-panels-border-resizing', undefined,
colors.lightGreen),
sidePanelOpenerFg: new CustomProp('theme-page-panels-side-panel-opener-fg', undefined,
colors.slate),
sidePanelOpenerActiveFg: new CustomProp('theme-page-panels-side-panel-opener-active-fg',
undefined, 'white'),
sidePanelOpenerActiveBg: new CustomProp('theme-page-panels-side-panel-opener-active-bg',
undefined, colors.lightGreen),
/* Add New */
addNewCircleFg: new CustomProp('theme-add-new-circle-fg', undefined, colors.light),
addNewCircleBg: new CustomProp('theme-add-new-circle-bg', undefined, colors.darkGreen),
addNewCircleHoverBg: new CustomProp('theme-add-new-circle-hover-bg', undefined,
colors.darkerGreen),
addNewCircleSmallFg: new CustomProp('theme-add-new-circle-small-fg', undefined, colors.light),
addNewCircleSmallBg: new CustomProp('theme-add-new-circle-small-bg', undefined,
colors.lightGreen),
addNewCircleSmallHoverBg: new CustomProp('theme-add-new-circle-small-hover-bg', undefined,
colors.darkGreen),
/* Top Bar */
topBarButtonPrimaryFg: new CustomProp('theme-top-bar-button-primary-fg', undefined,
colors.lightGreen),
topBarButtonSecondaryFg: new CustomProp('theme-top-bar-button-secondary-fg', undefined,
colors.slate),
topBarButtonDisabledFg: new CustomProp('theme-top-bar-button-disabled-fg', undefined,
colors.darkGrey),
topBarButtonErrorFg: new CustomProp('theme-top-bar-button-error-fg', undefined, colors.error),
/* Notifications */
notificationsPanelHeaderBg: new CustomProp('theme-notifications-panel-header-bg', undefined,
colors.lightGrey),
notificationsPanelBodyBg: new CustomProp('theme-notifications-panel-body-bg', undefined,
'white'),
notificationsPanelBorder: new CustomProp('theme-notifications-panel-border', undefined,
colors.darkGrey),
/* Toasts */
toastText: new CustomProp('theme-toast-text', undefined, colors.light),
toastLightText: new CustomProp('theme-toast-text-light', undefined, colors.slate),
toastBg: new CustomProp('theme-toast-bg', undefined, vars.toastBg),
toastErrorIcon: new CustomProp('theme-toast-error-icon', undefined, colors.error),
toastErrorBg: new CustomProp('theme-toast-error-bg', undefined, colors.error),
toastSuccessIcon: new CustomProp('theme-toast-success-icon', undefined, colors.darkGreen),
toastSuccessBg: new CustomProp('theme-toast-success-bg', undefined, colors.darkGreen),
toastWarningIcon: new CustomProp('theme-toast-warning-icon', undefined, colors.warning),
toastWarningBg: new CustomProp('theme-toast-warning-bg', undefined, colors.warningBg),
toastInfoIcon: new CustomProp('theme-toast-info-icon', undefined, colors.lightBlue),
toastInfoBg: new CustomProp('theme-toast-info-bg', undefined, colors.lightBlue),
toastControlFg: new CustomProp('theme-toast-control-fg', undefined, colors.lightGreen),
toastInfoControlFg: new CustomProp('theme-toast-control-info-fg', undefined, colors.lighterBlue),
/* Tooltips */
tooltipFg: new CustomProp('theme-tooltip-fg', undefined, 'white'),
tooltipBg: new CustomProp('theme-tooltip-bg', undefined, 'rgba(0, 0, 0, 0.75)'),
tooltipIcon: new CustomProp('theme-tooltip-icon', undefined, colors.slate),
tooltipCloseButtonFg: new CustomProp('theme-tooltip-close-button-fg', undefined, 'white'),
tooltipCloseButtonHoverFg: new CustomProp('theme-tooltip-close-button-hover-fg', undefined,
'black'),
tooltipCloseButtonHoverBg: new CustomProp('theme-tooltip-close-button-hover-bg', undefined,
'white'),
/* Modals */
modalBg: new CustomProp('theme-modal-bg', undefined, 'white'),
modalBackdrop: new CustomProp('theme-modal-backdrop', undefined, colors.backdrop),
modalBorder: new CustomProp('theme-modal-border', undefined, colors.mediumGreyOpaque),
modalBorderDark: new CustomProp('theme-modal-border-dark', undefined, colors.darkGrey),
modalBorderHover: new CustomProp('theme-modal-border-hover', undefined, colors.slate),
modalInnerShadow: new CustomProp('theme-modal-shadow-inner', undefined,
'rgba(31, 37, 50, 0.31)'),
modalOuterShadow: new CustomProp('theme-modal-shadow-outer', undefined,
'rgba(76, 86, 103, 0.24)'),
modalCloseButtonFg: new CustomProp('theme-modal-close-button-fg', undefined, colors.slate),
modalBackdropCloseButtonFg: new CustomProp('theme-modal-backdrop-close-button-fg', undefined,
vars.primaryBg),
modalBackdropCloseButtonHoverFg: new CustomProp('theme-modal-backdrop-close-button-hover-fg',
undefined, colors.lighterGreen),
/* Popups */
popupBg: new CustomProp('theme-popup-bg', undefined, 'white'),
popupInnerShadow: new CustomProp('theme-popup-shadow-inner', undefined,
'rgba(31, 37, 50, 0.31)'),
popupOuterShadow: new CustomProp('theme-popup-shadow-outer', undefined,
'rgba(76, 86, 103, 0.24)'),
popupCloseButtonFg: new CustomProp('theme-popup-close-button-fg', undefined, colors.slate),
/* Progress Bars */
progressBarFg: new CustomProp('theme-progress-bar-fg', undefined, colors.lightGreen),
progressBarErrorFg: new CustomProp('theme-progress-bar-error-fg', undefined, colors.error),
progressBarBg: new CustomProp('theme-progress-bar-bg', undefined, colors.darkGrey),
/* Links */
link: new CustomProp('theme-link', undefined, colors.lightGreen),
linkHover: new CustomProp('theme-link-hover', undefined, colors.lightGreen),
/* Hover */
hover: new CustomProp('theme-hover', undefined, colors.mediumGrey),
lightHover: new CustomProp('theme-hover-light', undefined, colors.lightGrey),
/* Cell Editor */
cellEditorFg: new CustomProp('theme-cell-editor-fg', undefined, colors.dark),
cellEditorBg: new CustomProp('theme-cell-editor-bg', undefined, colors.light),
/* Cursor */
cursor: new CustomProp('theme-cursor', undefined, colors.cursor),
cursorInactive: new CustomProp('theme-cursor-inactive', undefined, colors.inactiveCursor),
cursorReadonly: new CustomProp('theme-cursor-readonly', undefined, colors.slate),
/* Tables */
tableHeaderFg: new CustomProp('theme-table-header-fg', undefined, 'unset'),
tableHeaderSelectedFg: new CustomProp('theme-table-header-selected-fg', undefined, 'unset'),
tableHeaderBg: new CustomProp('theme-table-header-bg', undefined, colors.lightGrey),
tableHeaderSelectedBg: new CustomProp('theme-table-header-selected-bg', undefined,
colors.mediumGreyOpaque),
tableHeaderBorder: new CustomProp('theme-table-header-border', undefined, 'lightgray'),
tableHeaderBorderDark: new CustomProp('theme-table-header-border-dark', undefined,
colors.darkGrey),
tableBodyBg: new CustomProp('theme-table-body-bg', undefined, 'unset'),
tableBodyBorder: new CustomProp('theme-table-body-border', undefined, colors.darkGrey),
tableAddNewBg: new CustomProp('theme-table-add-new-bg', undefined, 'inherit'),
tableScrollShadow: new CustomProp('theme-table-scroll-shadow', undefined, '#444444'),
tableFrozenColumnsBorder: new CustomProp('theme-table-frozen-columns-border', undefined,
'#999999'),
tableDragDropIndicator: new CustomProp('theme-table-drag-drop-indicator', undefined, 'gray'),
tableDragDropShadow: new CustomProp('theme-table-drag-drop-shadow', undefined, '#F0F0F0'),
/* Cards */
cardCompactWidgetBg: new CustomProp('theme-card-compact-widget-bg', undefined,
colors.mediumGrey),
cardCompactRecordBg: new CustomProp('theme-card-compact-record-bg', undefined, 'white'),
cardBlocksBg: new CustomProp('theme-card-blocks-bg', undefined, colors.mediumGrey),
cardFormLabel: new CustomProp('theme-card-form-label', undefined, colors.slate),
cardCompactLabel: new CustomProp('theme-card-compact-label', undefined, colors.slate),
cardBlocksLabel: new CustomProp('theme-card-blocks-label', undefined, colors.slate),
cardFormBorder: new CustomProp('theme-card-form-border', undefined, 'lightgrey'),
cardCompactBorder: new CustomProp('theme-card-compact-border', undefined, colors.darkGrey),
cardEditingLayoutBg: new CustomProp('theme-card-editing-layout-bg', undefined,
'rgba(192, 192, 192, 0.2)'),
cardEditingLayoutBorder: new CustomProp('theme-card-editing-layout-border', undefined,
colors.darkGrey),
/* Card Lists */
cardListFormBorder: new CustomProp('theme-card-list-form-border', undefined, colors.darkGrey),
cardListBlocksBorder: new CustomProp('theme-card-list-blocks-border', undefined,
colors.darkGrey),
/* Selection */
selection: new CustomProp('theme-selection', undefined, colors.selection),
selectionOpaqueFg: new CustomProp('theme-selection-opaque-fg', undefined, 'unset'),
selectionOpaqueBg: new CustomProp('theme-selection-opaque-bg', undefined,
colors.selectionOpaque),
selectionOpaqueDarkBg: new CustomProp('theme-selection-opaque-dark-bg', undefined,
colors.selectionDarkerOpaque),
/* Widgets */
widgetBorder: new CustomProp('theme-widget-border', undefined, colors.darkGrey),
widgetActiveBorder: new CustomProp('theme-widget-active-border', undefined, colors.lightGreen),
widgetInactiveStripesLight: new CustomProp('theme-widget-inactive-stripes-light', undefined,
colors.lightGrey),
widgetInactiveStripesDark: new CustomProp('theme-widget-inactive-stripes-dark', undefined,
colors.mediumGreyOpaque),
/* Pinned Docs */
pinnedDocFooterBg: new CustomProp('theme-pinned-doc-footer-bg', undefined, colors.light),
pinnedDocBorder: new CustomProp('theme-pinned-doc-border', undefined, colors.mediumGrey),
pinnedDocBorderHover: new CustomProp('theme-pinned-doc-border-hover', undefined, colors.slate),
pinnedDocEditorBg: new CustomProp('theme-pinned-doc-editor-bg', undefined, colors.mediumGrey),
/* Raw Data */
rawDataTableBorder: new CustomProp('theme-raw-data-table-border', undefined, colors.mediumGrey),
rawDataTableBorderHover: new CustomProp('theme-raw-data-table-border-hover',
undefined, colors.slate),
/* Controls */
controlFg: new CustomProp('theme-control-fg', undefined, vars.controlFg),
controlPrimaryFg: new CustomProp('theme-control-primary-fg', undefined, vars.primaryFg),
controlPrimaryBg: new CustomProp('theme-control-primary-bg', undefined, vars.primaryBg),
controlSecondaryFg: new CustomProp('theme-control-secondary-fg', undefined, colors.slate),
controlHoverFg: new CustomProp('theme-control-hover-fg', undefined, vars.controlFgHover),
controlPrimaryHoverBg: new CustomProp('theme-control-primary-hover-bg', undefined,
vars.primaryBgHover),
controlSecondaryHoverFg: new CustomProp('theme-control-secondary-hover-fg', undefined,
colors.dark),
controlSecondaryHoverBg: new CustomProp('theme-control-secondary-hover-bg', undefined,
colors.darkGrey),
controlDisabledFg: new CustomProp('theme-control-disabled-fg', undefined, colors.light),
controlDisabledBg: new CustomProp('theme-control-disabled-bg', undefined, colors.slate),
controlPrimaryDisabled: new CustomProp('theme-control-primary-disabled', undefined,
colors.inactiveCursor),
controlBorder: new CustomProp('theme-control-border', undefined, '#11B683'),
/* Checkboxes */
checkboxBg: new CustomProp('theme-checkbox-bg', undefined, colors.light),
checkboxDisabledBg: new CustomProp('theme-checkbox-disabled-bg', undefined, colors.darkGrey),
checkboxBorder: new CustomProp('theme-checkbox-border', undefined, colors.darkGrey),
checkboxBorderHover: new CustomProp('theme-checkbox-border-hover', undefined, colors.hover),
/* Move Docs */
moveDocsSelectedFg: new CustomProp('theme-move-docs-selected-fg', undefined, 'white'),
moveDocsSelectedBg: new CustomProp('theme-move-docs-selected-bg', undefined, colors.lightGreen),
moveDocsDisabledFg: new CustomProp('theme-move-docs-disabled-bg', undefined, colors.darkGrey),
/* Filter Bar */
filterBarButtonSavedFg: new CustomProp('theme-filter-bar-button-saved-fg', undefined,
colors.light),
filterBarButtonSavedBg: new CustomProp('theme-filter-bar-button-saved-bg', undefined,
colors.slate),
filterBarButtonSavedHoverBg: new CustomProp('theme-filter-bar-button-saved-hover-bg', undefined,
colors.darkGrey),
/* Icon Buttons */
iconButtonFg: new CustomProp('theme-icon-button-fg', undefined, colors.light),
iconButtonPrimaryBg: new CustomProp('theme-icon-button-primary-bg', undefined,
colors.lightGreen),
iconButtonPrimaryHoverBg: new CustomProp('theme-icon-button-primary-hover-bg',
undefined, colors.darkGreen),
iconButtonSecondaryBg: new CustomProp('theme-icon-button-secondary-bg', undefined,
colors.darkGrey),
iconButtonSecondaryHoverBg: new CustomProp('theme-icon-button-secondary-hover-bg',
undefined, colors.slate),
/* Left Panel */
pageHoverBg: new CustomProp('theme-left-panel-page-hover-bg', undefined, colors.mediumGrey),
activePageFg: new CustomProp('theme-left-panel-active-page-fg', undefined, 'white'),
activePageBg: new CustomProp('theme-left-panel-active-page-bg', undefined, colors.darkBg),
disabledPageFg: new CustomProp('theme-left-panel-disabled-page-fg', undefined, colors.darkGrey),
pageOptionsFg: new CustomProp('theme-left-panel-page-options-bg', undefined, colors.slate),
pageOptionsHoverFg: new CustomProp('theme-left-panel-page-options-hover-fg', undefined, 'white'),
pageOptionsHoverBg: new CustomProp('theme-left-panel-page-options-hover-bg', undefined,
colors.darkGrey),
pageOptionsSelectedHoverBg: new CustomProp('theme-left-panel-page-options-selected-hover-bg',
undefined, colors.slate),
pageInitialsFg: new CustomProp('theme-left-panel-page-initials-fg', undefined, 'white'),
pageInitialsBg: new CustomProp('theme-left-panel-page-initials-bg', undefined, colors.slate),
/* Right Panel */
rightPanelTabFg: new CustomProp('theme-right-panel-tab-fg', undefined, colors.dark),
rightPanelTabBg: new CustomProp('theme-right-panel-tab-bg', undefined, colors.lightGrey),
rightPanelTabIcon: new CustomProp('theme-right-panel-tab-icon', undefined, colors.slate),
rightPanelTabIconHover: new CustomProp('theme-right-panel-tab-icon-hover', undefined,
colors.lightGreen),
rightPanelTabHoverBg: new CustomProp('theme-right-panel-tab-hover-bg', undefined,
colors.mediumGrey),
rightPanelTabSelectedFg: new CustomProp('theme-right-panel-tab-selected-fg', undefined,
colors.light),
rightPanelTabSelectedBg: new CustomProp('theme-right-panel-tab-selected-bg', undefined,
colors.lightGreen),
rightPanelTabCloseButtonHoverBg: new CustomProp('theme-right-panel-tab-close-button-hover-bg',
undefined, colors.darkGreen),
rightPanelSubtabFg: new CustomProp('theme-right-panel-subtab-fg', undefined, colors.lightGreen),
rightPanelSubtabSelectedFg: new CustomProp('theme-right-panel-subtab-selected-fg', undefined,
colors.dark),
rightPanelSubtabSelectedUnderline: new CustomProp('theme-right-panel-subtab-selected-underline',
undefined, colors.lightGreen),
rightPanelSubtabHoverFg: new CustomProp('theme-right-panel-subtab-hover-fg', undefined,
colors.darkGreen),
rightPanelSubtabHoverUnderline: new CustomProp('theme-right-panel-subtab-hover-underline',
undefined, colors.lightGreen),
rightPanelDisabledOverlay: new CustomProp('theme-right-panel-disabled-overlay', undefined,
'white'),
rightPanelToggleButtonEnabledFg: new CustomProp('theme-right-panel-toggle-button-enabled-fg',
undefined, colors.light),
rightPanelToggleButtonEnabledBg: new CustomProp('theme-right-panel-toggle-button-enabled-bg',
undefined, colors.dark),
rightPanelToggleButtonEnabledHoverFg: new CustomProp(
'theme-right-panel-toggle-button-enabled-hover-fg', undefined, colors.darkGrey),
rightPanelToggleButtonDisabledFg: new CustomProp('theme-right-panel-toggle-button-disabled-fg',
undefined, colors.light),
rightPanelToggleButtonDisabledBg: new CustomProp('theme-right-panel-toggle-button-disabled-bg',
undefined, colors.mediumGreyOpaque),
rightPanelFieldSettingsBg: new CustomProp('theme-right-panel-field-settings-bg',
undefined, colors.mediumGreyOpaque),
rightPanelFieldSettingsButtonBg: new CustomProp('theme-right-panel-field-settings-button-bg',
undefined, 'lightgrey'),
/* Document History */
documentHistorySnapshotFg: new CustomProp('theme-document-history-snapshot-fg', undefined,
colors.dark),
documentHistorySnapshotSelectedFg: new CustomProp('theme-document-history-snapshot-selected-fg',
undefined, colors.light),
documentHistorySnapshotBg: new CustomProp('theme-document-history-snapshot-bg', undefined,
'white'),
documentHistorySnapshotSelectedBg: new CustomProp('theme-document-history-snapshot-selected-bg',
undefined, colors.dark),
documentHistorySnapshotBorder: new CustomProp('theme-document-history-snapshot-border',
undefined, colors.mediumGrey),
documentHistoryActivityText: new CustomProp('theme-document-history-activity-text', undefined,
'unset'),
documentHistoryActivityLightText: new CustomProp('theme-document-history-activity-text-light',
undefined, '#333333'),
/* Accents */
accentIcon: new CustomProp('theme-accent-icon', undefined, colors.lightGreen),
accentBorder: new CustomProp('theme-accent-border', undefined, colors.lightGreen),
accentText: new CustomProp('theme-accent-text', undefined, colors.lightGreen),
/* Inputs */
inputFg: new CustomProp('theme-input-fg', undefined, 'black'),
inputBg: new CustomProp('theme-input-bg', undefined, 'white'),
inputDisabledFg: new CustomProp('theme-input-disabled-fg', undefined, colors.slate),
inputDisabledBg: new CustomProp('theme-input-disabled-bg', undefined, colors.lightGrey),
inputPlaceholderFg: new CustomProp('theme-input-placeholder-fg', undefined, '#757575'),
inputBorder: new CustomProp('theme-input-border', undefined, colors.darkGrey),
inputValid: new CustomProp('theme-input-valid', undefined, colors.lightGreen),
inputInvalid: new CustomProp('theme-input-invalid', undefined, colors.error),
inputFocus: new CustomProp('theme-input-focus', undefined, '#5E9ED6'),
inputReadonlyBg: new CustomProp('theme-input-readonly-bg', undefined, colors.lightGrey),
inputReadonlyBorder: new CustomProp('theme-input-readonly-border', undefined, colors.mediumGreyOpaque),
/* Choice Entry */
choiceEntryBg: new CustomProp('theme-choice-entry-bg', undefined, 'white'),
choiceEntryBorder: new CustomProp('theme-choice-entry-border', undefined, colors.darkGrey),
choiceEntryBorderHover: new CustomProp('theme-choice-entry-border-hover', undefined,
colors.hover),
/* Select Buttons */
selectButtonFg: new CustomProp('theme-select-button-fg', undefined, colors.dark),
selectButtonPlaceholderFg: new CustomProp('theme-select-button-placeholder-fg', undefined,
colors.slate),
selectButtonDisabledFg: new CustomProp('theme-select-button-disabled-fg', undefined, 'grey'),
selectButtonBg: new CustomProp('theme-select-button-bg', undefined, 'white'),
selectButtonBorder: new CustomProp('theme-select-button-border', undefined, colors.darkGrey),
selectButtonBorderInvalid: new CustomProp('theme-select-button-border-invalid', undefined,
colors.error),
/* Menus */
menuText: new CustomProp('theme-menu-text', undefined, colors.slate),
menuLightText: new CustomProp('theme-menu-light-text', undefined, colors.slate),
menuBg: new CustomProp('theme-menu-bg', undefined, 'white'),
menuSubheaderFg: new CustomProp('theme-menu-subheader-fg', undefined, 'unset'),
menuBorder: new CustomProp('theme-menu-border', undefined, colors.mediumGreyOpaque),
menuShadow: new CustomProp('theme-menu-shadow', undefined, 'rgba(38, 38, 51, 0.6)'),
/* Menu Items */
menuItemFg: new CustomProp('theme-menu-item-fg', undefined, 'unset'),
menuItemSelectedFg: new CustomProp('theme-menu-item-selected-fg', undefined, colors.light),
menuItemSelectedBg: new CustomProp('theme-menu-item-selected-bg', undefined, vars.primaryBg),
menuItemDisabledFg: new CustomProp('theme-menu-item-disabled-fg', undefined, '#D9D9D9'),
menuItemIconFg: new CustomProp('theme-menu-item-icon-fg', undefined, colors.slate),
menuItemIconSelectedFg: new CustomProp('theme-menu-item-icon-selected-fg', undefined, 'white'),
menuItemLinkFg: new CustomProp('theme-menu-item-link-fg', undefined, colors.lightGreen),
menuItemLinkSelectedFg: new CustomProp('theme-menu-item-link-selected-fg', undefined,
colors.darkGreen),
menuItemLinkselectedBg: new CustomProp('theme-menu-item-link-selected-bg', undefined,
colors.mediumGreyOpaque),
/* Autocomplete */
autocompleteMatchText: new CustomProp('theme-autocomplete-match-text', undefined,
colors.lightGreen),
autocompleteSelectedMatchText: new CustomProp('theme-autocomplete-selected-match-text',
undefined, colors.lighterGreen),
autocompleteChoiceSelectedBg: new CustomProp('theme-autocomplete-item-selected-bg', undefined,
colors.mediumGreyOpaque),
/* Search */
searchBorder: new CustomProp('theme-search-border', undefined, 'grey'),
searchPrevNextButtonFg: new CustomProp('theme-search-prev-next-button-fg', undefined,
colors.slate),
searchPrevNextButtonBg: new CustomProp('theme-search-prev-next-button-bg', undefined,
colors.mediumGrey),
/* Loaders */
loaderFg: new CustomProp('theme-loader-fg', undefined, colors.lightGreen),
loaderBg: new CustomProp('theme-loader-bg', undefined, colors.darkGrey),
/* Site Switcher */
siteSwitcherActiveFg: new CustomProp('theme-site-switcher-active-fg', undefined, colors.light),
siteSwitcherActiveBg: new CustomProp('theme-site-switcher-active-bg', undefined, colors.dark),
/* Doc Menu */
docMenuDocOptionsFg: new CustomProp('theme-doc-menu-doc-options-fg', undefined, colors.darkGrey),
docMenuDocOptionsHoverFg: new CustomProp('theme-doc-menu-doc-options-hover-fg', undefined,
colors.slate),
docMenuDocOptionsHoverBg: new CustomProp('theme-doc-menu-doc-options-hover-bg', undefined,
colors.darkGrey),
/* Shortcut Keys */
shortcutKeyFg: new CustomProp('theme-shortcut-key-fg', undefined, 'black'),
shortcutKeyPrimaryFg: new CustomProp('theme-shortcut-key-primary-fg', undefined,
colors.darkGreen),
shortcutKeySecondaryFg: new CustomProp('theme-shortcut-key-secondary-fg', undefined,
colors.slate),
shortcutKeyBg: new CustomProp('theme-shortcut-key-bg', undefined, 'white'),
shortcutKeyBorder: new CustomProp('theme-shortcut-key-border', undefined, colors.slate),
/* Breadcrumbs */
breadcrumbsTagFg: new CustomProp('theme-breadcrumbs-tag-fg', undefined, 'white'),
breadcrumbsTagBg: new CustomProp('theme-breadcrumbs-tag-bg', undefined, colors.slate),
breadcrumbsTagAlertBg: new CustomProp('theme-breadcrumbs-tag-alert-fg', undefined, colors.error),
/* Page Widget Picker */
widgetPickerPrimaryBg: new CustomProp('theme-widget-picker-primary-bg', undefined, 'white'),
widgetPickerSecondaryBg: new CustomProp('theme-widget-picker-secondary-bg', undefined,
colors.lightGrey),
widgetPickerItemFg: new CustomProp('theme-widget-picker-item-fg', undefined, colors.lightGrey),
widgetPickerItemSelectedBg: new CustomProp('theme-widget-picker-item-selected-bg', undefined,
colors.lightGrey),
widgetPickerItemDisabledBg: new CustomProp('theme-widget-picker-item-disabled-bg', undefined,
colors.lightGrey),
widgetPickerIcon: new CustomProp('theme-widget-picker-icon', undefined, colors.slate),
widgetPickerPrimaryIcon: new CustomProp('theme-widget-picker-primary-icon', undefined,
colors.lightGreen),
widgetPickerSummaryIcon: new CustomProp('theme-widget-picker-summary-icon', undefined,
colors.darkGreen),
widgetPickerBorder: new CustomProp('theme-widget-picker-border', undefined, colors.mediumGrey),
widgetPickerShadow: new CustomProp('theme-widget-picker-shadow', undefined,
'rgba(38,38,51,0.20)'),
/* Code View */
codeViewText: new CustomProp('theme-code-view-text', undefined, '#444'),
codeViewKeyword: new CustomProp('theme-code-view-keyword', undefined, '#444'),
codeViewComment: new CustomProp('theme-code-view-comment', undefined, '#888888'),
codeViewMeta: new CustomProp('theme-code-view-meta', undefined, '#1F7199'),
codeViewTitle: new CustomProp('theme-code-view-title', undefined, '#880000'),
codeViewParams: new CustomProp('theme-code-view-params', undefined, '#444'),
codeViewString: new CustomProp('theme-code-view-string', undefined, '#880000'),
codeViewNumber: new CustomProp('theme-code-view-number', undefined, '#880000'),
/* Importer */
importerTableInfoBorder: new CustomProp('theme-importer-table-info-border', undefined, colors.darkGrey),
importerPreviewBorder: new CustomProp('theme-importer-preview-border', undefined,
colors.darkGrey),
importerSkippedTableOverlay: new CustomProp('theme-importer-skipped-table-overlay', undefined,
colors.mediumGrey),
importerMatchIcon: new CustomProp('theme-importer-match-icon', undefined, colors.darkGrey),
/* Menu Toggles */
menuToggleFg: new CustomProp('theme-menu-toggle-fg', undefined, colors.slate),
menuToggleHoverFg: new CustomProp('theme-menu-toggle-hover-fg', undefined, colors.darkGreen),
menuToggleActiveFg: new CustomProp('theme-menu-toggle-active-fg', undefined, colors.darkerGreen),
menuToggleBg: new CustomProp('theme-menu-toggle-bg', undefined, 'white'),
menuToggleBorder: new CustomProp('theme-menu-toggle-border', undefined, colors.slate),
/* Button Groups */
buttonGroupFg: new CustomProp('theme-button-group-fg', undefined, colors.dark),
buttonGroupLightFg: new CustomProp('theme-button-group-light-fg', undefined, colors.slate),
buttonGroupBg: new CustomProp('theme-button-group-bg', undefined, 'unset'),
buttonGroupIcon: new CustomProp('theme-button-group-icon', undefined, colors.slate),
buttonGroupBorder: new CustomProp('theme-button-group-border', undefined, colors.darkGrey),
buttonGroupBorderHover: new CustomProp('theme-button-group-border-hover', undefined,
colors.hover),
buttonGroupSelectedFg: new CustomProp('theme-button-group-selected-fg', undefined, colors.light),
buttonGroupLightSelectedFg: new CustomProp('theme-button-group-light-selected-fg', undefined,
colors.lightGreen),
buttonGroupSelectedBg: new CustomProp('theme-button-group-selected-bg', undefined, colors.dark),
buttonGroupSelectedBorder: new CustomProp('theme-button-group-selected-border', undefined,
colors.dark),
/* Access Rules */
accessRulesTableHeaderFg: new CustomProp('theme-access-rules-table-header-fg', undefined,
colors.dark),
accessRulesTableHeaderBg: new CustomProp('theme-access-rules-table-header-bg', undefined,
colors.mediumGrey),
accessRulesTableBodyFg: new CustomProp('theme-access-rules-table-body-fg', undefined,
colors.dark),
accessRulesTableBorder: new CustomProp('theme-access-rules-table-border', undefined,
colors.slate),
/* Cells */
cellFg: new CustomProp('theme-cell-fg', undefined, 'unset'),
cellBg: new CustomProp('theme-cell-bg', undefined, '#FFFFFF00'),
cellZebraBg: new CustomProp('theme-cell-zebra-bg', undefined, '#F8F8F8'),
/* Formula Editor */
formulaEditorBg: new CustomProp('theme-formula-editor-bg', undefined, 'white'),
/* Charts */
chartFg: new CustomProp('theme-chart-fg', undefined, '#444'),
chartBg: new CustomProp('theme-chart-bg', undefined, '#fff'),
chartLegendBg: new CustomProp('theme-chart-legend-bg', undefined, '#FFFFFF80'),
chartXAxis: new CustomProp('theme-chart-x-axis', undefined, '#444'),
chartYAxis: new CustomProp('theme-chart-y-axis', undefined, '#444'),
};
const cssColors = values(colors).map(v => v.decl()).join('\n');
const cssVars = values(vars).map(v => v.decl()).join('\n');
const cssFontParams = `
@@ -237,17 +759,107 @@ export function isScreenResizing(): Observable<boolean> {
return _isScreenResizingObs;
}
let _prefersDarkModeObs: Observable<boolean>|undefined;
/**
* Attaches the global css properties to the document's root to them available in the page.
* Returns a singleton observable for whether the user agent prefers dark mode.
*/
export function prefersDarkModeObs(): Observable<boolean> {
if (!_prefersDarkModeObs) {
const query = window.matchMedia('(prefers-color-scheme: dark)');
const obs = Observable.create<boolean>(null, query.matches);
query.addEventListener('change', event => obs.set(event.matches));
_prefersDarkModeObs = obs;
}
return _prefersDarkModeObs;
}
/**
* 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));
document.documentElement.classList.add(cssRoot.className);
document.body.classList.add(cssBody.className);
const theme = getTheme(productFlavor);
if (theme.bodyClassName) {
document.body.classList.add(theme.bodyClassName);
const customTheme = getTheme(productFlavor);
if (customTheme.bodyClassName) {
document.body.classList.add(customTheme.bodyClassName);
}
const interfaceStyle = urlState().state.get().params?.style || 'full';
document.body.classList.add(`interface-${interfaceStyle}`);
}
/**
* Attaches theme-related css properties to the theme style element.
*/
export 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};`);
// Include properties for styling the scrollbar.
properties.push(...getCssScrollbarProperties(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 {
${properties.join('\n')}
}`;
// Make the browser aware of the color scheme.
document.documentElement.style.setProperty(`color-scheme`, appearance);
// Cache the appearance in local storage; this is currently used to apply a suitable
// background image that's shown while the application is loading.
localStorage.setItem('appearance', appearance);
}
/**
* Gets scrollbar-related css properties that are appropriate for the given `appearance`.
*
* Note: Browser support for customizing scrollbars is still a mixed bag; the bulk of customization
* is non-standard and unsupported by Firefox. If support matures, we could expose some of these in
* custom themes, but for now we'll just go with reasonable presets.
*/
function getCssScrollbarProperties(appearance: ThemeAppearance) {
return [
'--scroll-bar-fg: ' +
(appearance === 'dark' ? '#6B6B6B;' : '#A8A8A8;'),
'--scroll-bar-hover-fg: ' +
(appearance === 'dark' ? '#7B7B7B;' : '#8F8F8F;'),
'--scroll-bar-active-fg: ' +
(appearance === 'dark' ? '#8B8B8B;' : '#7C7C7C;'),
'--scroll-bar-bg: ' +
(appearance === 'dark' ? '#2B2B2B;' : '#F0F0F0;'),
];
}
/**
* Gets background-related css properties that are appropriate for the given `appearance`.
*
* Currently, this sets a property for showing a background image that's visible while a page
* is loading.
*/
function getCssThemeBackgroundProperties(appearance: ThemeAppearance) {
const value = appearance === 'dark'
? 'url("img/prismpattern.png")'
: '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;
}

View File

@@ -1,4 +1,4 @@
import {testId} from 'app/client/ui2018/cssVars';
import {testId, theme} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {styled} from 'grainjs';
@@ -6,6 +6,7 @@ import {styled} from 'grainjs';
// Drag icon for use in koForm draggableList.
export const cssDragger = styled((...args: any[]) => icon('DragDrop', testId('dragger'), ...args), `
--icon-color: ${theme.controlSecondaryFg};
visibility: hidden;
align-self: center;
flex-shrink: 0;

View File

@@ -9,7 +9,7 @@
*
* TODO: Consider merging this into grainjs's input widget.
*/
import { colors } from 'app/client/ui2018/cssVars';
import { theme } from 'app/client/ui2018/cssVars';
import { dom, DomArg, styled } from 'grainjs';
import { Observable } from 'grainjs';
import noop = require('lodash/noop');
@@ -45,7 +45,7 @@ export const cssLabelText = styled(rawTextInput, `
export const cssTextInput = styled('input', `
outline: none;
height: 28px;
border: 1px solid ${colors.darkGrey};
border: 1px solid ${theme.inputBorder};
border-radius: 3px;
padding: 0 6px;
`);
@@ -97,7 +97,7 @@ export function editableLabel(label: Observable<string>, options: EditableLabelO
/**
* Provides a text input element that pretty much behaves like the editableLabel only it shows as a
* regular input within a rigid static frame. It takes in an observable that is setf on Enter or loss
* regular input within a rigid static frame. It takes in an observable that is set on Enter or loss
* of focus. Escape cancels editing. Validation logic (if any) should happen in the save function,
* to reject a value simply throw an error, this will revert to the the saved one.
*/

View File

@@ -49,7 +49,7 @@
* `);
*/
import { colors } from 'app/client/ui2018/cssVars';
import { theme } from 'app/client/ui2018/cssVars';
import { dom, DomElementArg, styled } from 'grainjs';
import { IconName } from './IconList';
@@ -65,7 +65,7 @@ const iconDiv = styled('div', `
-webkit-mask-size: contain;
width: 16px;
height: 16px;
background-color: var(--icon-color, black);
background-color: var(--icon-color, var(--grist-theme-text, black));
`);
export const cssIconBackground = styled(iconDiv, `
@@ -85,7 +85,7 @@ export function icon(name: IconName, ...domArgs: DomElementArg[]): HTMLElement {
}
/**
* Container box for an slate-colored icon to serve as a button, with a grey background on hover.
* Container box for an icon to serve as a button..
*/
export const cssIconButton = styled('div', `
flex: none;
@@ -95,9 +95,9 @@ export const cssIconButton = styled('div', `
border-radius: 3px;
line-height: 0px;
cursor: default;
--icon-color: ${colors.slate};
--icon-color: ${theme.controlSecondaryFg};
&:hover, &.weasel-popup-open {
background-color: ${colors.darkGrey};
--icon-color: ${colors.slate};
background-color: ${theme.controlSecondaryHoverBg};
--icon-color: ${theme.controlSecondaryFg};
}
`);

View File

@@ -1,19 +1,19 @@
import { sameDocumentUrlState, urlState } from 'app/client/models/gristUrlState';
import { colors } from 'app/client/ui2018/cssVars';
import { theme } from 'app/client/ui2018/cssVars';
import { CellValue } from 'app/plugin/GristData';
import { dom, IDomArgs, Observable, styled } from 'grainjs';
/**
* Styling for a simple green <A HREF> link.
* Styling for a simple <A HREF> link.
*/
export const cssLink = styled('a', `
color: ${colors.lightGreen};
--icon-color: ${colors.lightGreen};
color: ${theme.link};
--icon-color: ${theme.link};
text-decoration: none;
&:hover, &:focus {
color: ${colors.lightGreen};
--icon-color: ${colors.lightGreen};
color: ${theme.linkHover};
--icon-color: ${theme.linkHover};
text-decoration: underline;
}
`);

View File

@@ -1,4 +1,4 @@
import {colors} from 'app/client/ui2018/cssVars';
import {theme} from 'app/client/ui2018/cssVars';
import {DomArg, keyframes, styled} from 'grainjs';
const rotate360 = keyframes(`
@@ -9,10 +9,10 @@ const rotate360 = keyframes(`
const flash = keyframes(`
0% {
background-color: ${colors.lightGreen};
background-color: ${theme.loaderFg};
}
50%, 100% {
background-color: ${colors.darkGrey};
background-color: ${theme.loaderBg};
}
`);
@@ -25,8 +25,8 @@ export const loadingSpinner = styled('div', `
width: 32px;
height: 32px;
border-radius: 32px;
border: 4px solid ${colors.darkGrey};
border-top-color: ${colors.lightGreen};
border: 4px solid ${theme.loaderBg};
border-top-color: ${theme.loaderFg};
animation: ${rotate360} 1s ease-out infinite;
`);
@@ -52,8 +52,8 @@ const cssLoadingDot = styled('div', `
border-radius: 50%;
width: var(--dot-size);
height: var(--dot-size);
background-color: ${colors.lightGreen};
color: ${colors.lightGreen};
background-color: ${theme.loaderFg};
color: ${theme.loaderFg};
animation: ${flash} 1s alternate infinite;
&-left {

View File

@@ -2,7 +2,7 @@ import { Command } from 'app/client/components/commands';
import { NeedUpgradeError, reportError } from 'app/client/models/errors';
import { textButton } from 'app/client/ui2018/buttons';
import { cssCheckboxSquare, cssLabel, cssLabelText } from 'app/client/ui2018/checkbox';
import { colors, testId, vars } from 'app/client/ui2018/cssVars';
import { testId, theme, vars } from 'app/client/ui2018/cssVars';
import { IconName } from 'app/client/ui2018/IconList';
import { icon } from 'app/client/ui2018/icons';
import { cssSelectBtn } from 'app/client/ui2018/select';
@@ -60,11 +60,12 @@ export const cssMenuElem = styled('div', `
line-height: initial;
max-width: 400px;
padding: 8px 0px 16px 0px;
box-shadow: 0 2px 20px 0 rgba(38,38,51,0.6);
box-shadow: 0 2px 20px 0 ${theme.menuShadow};
min-width: 160px;
z-index: 999;
--weaseljs-selected-background-color: ${vars.primaryBg};
--weaseljs-selected-background-color: ${theme.menuItemSelectedBg};
--weaseljs-menu-item-padding: 8px 24px;
background-color: ${theme.menuBg};
@media print {
& {
@@ -76,13 +77,16 @@ export const cssMenuElem = styled('div', `
const menuItemStyle = `
justify-content: flex-start;
align-items: center;
--icon-color: ${colors.lightGreen};
color: ${theme.menuItemFg};
--icon-color: ${theme.accentIcon};
.${weasel.cssMenuItem.className}-sel {
--icon-color: ${colors.light};
color: ${theme.menuItemSelectedFg};
--icon-color: ${theme.menuItemSelectedFg};
}
&.disabled {
cursor: default;
opacity: 0.2;
color: ${theme.menuItemDisabledFg};
--icon-color: ${theme.menuItemDisabledFg};
}
`;
@@ -237,7 +241,9 @@ export function multiSelect<T>(selectedOptions: MutableObsArray<T>,
weasel.setPopupToCreateDom(elem, ctl => buildMultiSelectMenu(ctl), weasel.defaultMenuOptions);
},
dom.style('border', use => {
return options.error && use(options.error) ? '1px solid red' : `1px solid ${colors.darkGrey}`;
return options.error && use(options.error)
? `1px solid ${theme.selectButtonBorderInvalid}`
: `1px solid ${theme.selectButtonBorder}`;
}),
...domArgs
);
@@ -394,6 +400,7 @@ export function selectOption(
}
export const menuSubHeader = styled('div', `
color: ${theme.menuSubheaderFg};
font-size: ${vars.xsmallFontSize};
text-transform: uppercase;
font-weight: ${vars.bigControlTextWeight};
@@ -405,7 +412,7 @@ export const menuText = styled('div', `
display: flex;
align-items: center;
font-size: ${vars.smallFontSize};
color: ${colors.slate};
color: ${theme.menuText};
padding: 8px 24px 4px 24px;
max-width: 250px;
cursor: default;
@@ -440,6 +447,7 @@ export function menuAnnotate(text: string, ...args: DomElementArg[]) {
}
export const menuDivider = styled(weasel.cssMenuDivider, `
background-color: ${theme.menuBorder};
margin: 8px 0;
`);
@@ -464,8 +472,8 @@ const cssSelectBtnLink = styled('div', `
display: flex;
align-items: center;
font-size: ${vars.mediumFontSize};
color: ${colors.lightGreen};
--icon-color: ${colors.lightGreen};
color: ${theme.controlFg};
--icon-color: ${theme.controlFg};
width: initial;
height: initial;
line-height: inherit;
@@ -480,8 +488,8 @@ const cssSelectBtnLink = styled('div', `
-moz-appearance: none;
&:hover, &:focus, &:active {
color: ${colors.darkGreen};
--icon-color: ${colors.darkGreen};
color: ${theme.controlHoverFg};
--icon-color: ${theme.controlHoverFg};
box-shadow: initial;
}
`);
@@ -489,7 +497,7 @@ const cssSelectBtnLink = styled('div', `
const cssOptionIcon = styled(icon, `
height: 16px;
width: 16px;
background-color: ${colors.slate};
background-color: ${theme.menuItemIconFg};
margin: -3px 8px 0 2px;
`);
@@ -504,7 +512,11 @@ export const cssOptionRowIcon = styled(cssOptionIcon, `
flex: none;
.${weasel.cssMenuItem.className}-sel & {
background-color: white;
background-color: ${theme.menuItemSelectedFg};
}
.${weasel.cssMenuItem.className}.disabled & {
background-color: ${theme.menuItemDisabledFg};
}
`);
@@ -512,6 +524,19 @@ const cssOptionLabel = styled('div', `
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.${weasel.cssMenuItem.className} & {
color: ${theme.menuItemFg};
}
.${weasel.cssMenuItem.className}-sel & {
color: ${theme.menuItemSelectedFg};
background-color: ${theme.menuItemSelectedBg};
}
.${weasel.cssMenuItem.className}.disabled & {
color: ${theme.menuItemDisabledFg};
}
`);
const cssInlineCollapseIcon = styled(icon, `
@@ -524,7 +549,7 @@ const cssCollapseIcon = styled(icon, `
right: 12px;
top: calc(50% - 8px);
pointer-events: none;
background-color: ${colors.dark};
background-color: ${theme.selectButtonFg};
`);
const cssInputButtonMenuElem = styled(cssMenuElem, `
@@ -537,16 +562,20 @@ const cssMenuItemCmd = styled('div', `
const cssCmdKey = styled('span', `
margin-left: 16px;
color: ${colors.slate};
color: ${theme.menuItemIconFg};
margin-right: -12px;
.${weasel.cssMenuItem.className}-sel > & {
color: ${colors.lightGrey};
color: ${theme.menuItemIconSelectedFg};
}
.${weasel.cssMenuItem.className}.disabled > & {
color: ${theme.menuItemDisabledFg};
}
`);
const cssAnnotateMenuItem = styled('span', `
color: ${colors.lightGreen};
color: ${theme.accentText};
text-transform: uppercase;
font-size: 8px;
vertical-align: super;
@@ -555,7 +584,7 @@ const cssAnnotateMenuItem = styled('span', `
font-weight: bold;
.${weasel.cssMenuItem.className}-sel > & {
color: white;
color: ${theme.menuItemIconSelectedFg};
}
`);
@@ -563,9 +592,10 @@ const cssMultiSelectSummary = styled('div', `
flex: 1 1 0px;
overflow: hidden;
text-overflow: ellipsis;
color: ${theme.selectButtonFg};
&-placeholder {
color: ${colors.slate}
color: ${theme.selectButtonPlaceholderFg};
}
`);
@@ -575,6 +605,7 @@ const cssMultiSelectMenu = styled(weasel.cssMenu, `
max-height: calc(max(300px, 95vh - 300px));
max-width: 400px;
padding-bottom: 0px;
background-color: ${theme.menuBg};
`);
const cssCheckboxLabel = styled(cssLabel, `
@@ -583,7 +614,7 @@ const cssCheckboxLabel = styled(cssLabel, `
const cssCheckboxText = styled(cssLabelText, `
margin-right: 12px;
color: ${colors.dark};
color: ${theme.text};
white-space: pre;
`);

View File

@@ -3,7 +3,7 @@ import {reportError} from 'app/client/models/errors';
import {cssInput} from 'app/client/ui/cssInput';
import {prepareForTransition, TransitionWatcher} from 'app/client/ui/transitions';
import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/buttons';
import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars';
import {mediaSmall, testId, theme, vars} from 'app/client/ui2018/cssVars';
import {loadingSpinner} from 'app/client/ui2018/loaders';
import {waitGrainObs} from 'app/common/gutil';
import {Computed, Disposable, dom, DomContents, DomElementArg, input, keyframes,
@@ -474,12 +474,12 @@ export function cssModalWidth(style: ModalWidth) {
// the flex container, to ensure the full item can be scrolled in case of overflow.
// See https://stackoverflow.com/a/33455342/328565
const cssModalDialog = styled('div', `
background-color: white;
background-color: ${theme.modalBg};
min-width: 428px;
color: black;
color: ${theme.darkText};
margin: auto;
border-radius: 3px;
box-shadow: 0 2px 18px 0 rgba(31,37,50,0.31), 0 0 1px 0 rgba(76,86,103,0.24);
box-shadow: 0 2px 18px 0 ${theme.modalInnerShadow}, 0 0 1px 0 ${theme.modalOuterShadow};
padding: 40px 64px;
outline: none;
@@ -506,13 +506,14 @@ const cssModalDialog = styled('div', `
export const cssModalTitle = styled('div', `
font-size: ${vars.xxxlargeFontSize};
font-weight: ${vars.headerControlTextWeight};
color: ${colors.dark};
color: ${theme.text};
margin: 0 0 16px 0;
line-height: 32px;
overflow-wrap: break-word;
`);
export const cssModalBody = styled('div', `
color: ${theme.text};
margin: 16px 0;
`);
@@ -530,7 +531,7 @@ const cssFadeIn = keyframes(`
`);
const cssFadeOut = keyframes(`
from {background-color: ${colors.backdrop}}
from {background-color: ${theme.modalBackdrop}}
`);
const cssModalBacker = styled('div', `
@@ -543,7 +544,7 @@ const cssModalBacker = styled('div', `
left: 0;
padding: 16px;
z-index: 999;
background-color: ${colors.backdrop};
background-color: ${theme.modalBackdrop};
overflow-y: auto;
animation-name: ${cssFadeIn};
animation-duration: 0.4s;

View File

@@ -1,7 +1,7 @@
import { isDesktop } from 'app/client/lib/browserInfo';
import { cssEditorInput } from "app/client/ui/HomeLeftPane";
import { itemHeader, itemHeaderWrapper, treeViewContainer } from "app/client/ui/TreeViewComponentCss";
import { colors } from "app/client/ui2018/cssVars";
import { theme } from "app/client/ui2018/cssVars";
import { icon } from "app/client/ui2018/icons";
import { menu, menuItem, menuText } from "app/client/ui2018/menus";
import { dom, domComputed, DomElementArg, makeTestId, observable, Observable, styled } from "grainjs";
@@ -86,7 +86,7 @@ export function buildPageDom(name: Observable<string>, actions: PageActions, ...
dom.on('click', (ev) => isTargetSelected(ev.target as HTMLElement) && isRenaming.set(true)),
),
cssPageMenuTrigger(
cssPageIcon('Dots'),
cssPageMenuIcon('Dots'),
menu(pageMenu, {placement: 'bottom-start', parentSelectorToMark: '.' + itemHeader.className}),
dom.on('click', (ev) => { ev.stopPropagation(); ev.preventDefault(); }),
@@ -104,7 +104,6 @@ export function buildPageDom(name: Observable<string>, actions: PageActions, ...
}
const cssPageItem = styled('a', `
--icon-color: ${colors.slate};
display: flex;
flex-direction: row;
height: 28px;
@@ -122,9 +121,9 @@ const cssPageItem = styled('a', `
const cssPageInitial = styled('div', `
flex-shrink: 0;
color: white;
color: ${theme.pageInitialsFg};
border-radius: 3px;
background-color: ${colors.slate};
background-color: ${theme.pageInitialsBg};
width: 16px;
height: 16px;
text-align: center;
@@ -187,20 +186,21 @@ const cssPageMenuTrigger = styled('div', `
}
}
.${itemHeaderWrapper.className}-not-dragging &:hover, &.weasel-popup-open {
background-color: ${colors.darkGrey};
background-color: ${theme.pageOptionsHoverBg};
}
.${itemHeaderWrapper.className}-not-dragging > .${itemHeader.className}.selected &:hover,
.${itemHeaderWrapper.className}-not-dragging > .${itemHeader.className}.selected &.weasel-popup-open {
background-color: ${colors.slate};
background-color: ${theme.pageOptionsSelectedHoverBg};
}
.${itemHeader.className}.weasel-popup-open, .${itemHeader.className}-renaming {
background-color: ${colors.mediumGrey};
background-color: ${theme.pageHoverBg};
}
`);
const cssPageIcon = styled(icon, `
const cssPageMenuIcon = styled(icon, `
background-color: ${theme.pageOptionsFg};
.${itemHeader.className}.selected & {
background-color: white;
background-color: ${theme.pageOptionsHoverFg};
}
`);

View File

@@ -8,7 +8,7 @@ import { SearchModel } from 'app/client/models/SearchModel';
import { hoverTooltip, IHoverTipOptions } from 'app/client/ui/tooltips';
import { cssHoverCircle, cssTopBarBtn } from 'app/client/ui/TopBarCss';
import { labeledSquareCheckbox } from 'app/client/ui2018/checkbox';
import { colors, mediaSmall, vars } from 'app/client/ui2018/cssVars';
import { mediaSmall, theme, vars } from 'app/client/ui2018/cssVars';
import { icon } from 'app/client/ui2018/icons';
import { dom, input, styled } from 'grainjs';
import { noTestId, TestId } from 'grainjs';
@@ -32,7 +32,7 @@ const searchWrapper = styled('div', `
position: relative;
&-expand {
width: 100% !important;
border: 1px solid grey;
border: 1px solid ${theme.searchBorder};
}
@media ${mediaSmall} {
& {
@@ -58,6 +58,8 @@ const expandedSearch = styled('div', `
`);
const searchInput = styled(input, `
background-color: ${theme.topHeaderBg};
color: ${theme.inputFg};
outline: none;
border: none;
margin: 0;
@@ -70,6 +72,9 @@ const searchInput = styled(input, `
.${searchWrapper.className}-expand & {
width: 100%;
}
&::placeholder {
color: ${theme.inputPlaceholderFg};
}
`);
const cssArrowBtn = styled('div', `
@@ -80,8 +85,8 @@ const cssArrowBtn = styled('div', `
visibility: hidden;
width: 24px;
height: 24px;
background-color: ${colors.mediumGrey};
--icon-color: ${colors.slate};
background-color: ${theme.searchPrevNextButtonBg};
--icon-color: ${theme.searchPrevNextButtonFg};
border-radius: 3px;
text-align: center;
display: flex;
@@ -94,31 +99,31 @@ const cssArrowBtn = styled('div', `
const cssCloseBtn = styled(icon, `
cursor: pointer;
background-color: ${colors.lightGreen};
background-color: ${theme.controlFg};
margin-left: 4px;
flex-shrink: 0;
`);
const cssLabel = styled('span', `
font-size: ${vars.smallFontSize};
color: ${colors.slate};
color: ${theme.lightText};
white-space: nowrap;
margin-right: 12px;
`);
const cssOptions = styled('div', `
background: ${theme.topHeaderBg};
position: absolute;
right: 0;
top: 46px;
top: 48px;
z-index: 1;
background: white;
padding: 2px 4px;
overflow: hidden;
white-space: nowrap;
`);
const cssShortcut = styled('span', `
color: ${colors.slate};
color: ${theme.lightText};
`);
const searchArrowBtnTooltipOptions: IHoverTipOptions = {

View File

@@ -1,4 +1,4 @@
import {colors, vars} from 'app/client/ui2018/cssVars';
import {theme, vars} from 'app/client/ui2018/cssVars';
import {styled} from 'grainjs';
// Import popweasel so that the styles we define here are included later in CSS, and take priority
@@ -19,12 +19,12 @@ export const cssSelectBtn = styled('div', `
width: 100%;
height: 30px;
line-height: 16px;
background-color: white;
color: ${colors.dark};
--icon-color: ${colors.dark};
background-color: ${theme.selectButtonBg};
color: ${theme.selectButtonFg};
--icon-color: ${theme.selectButtonFg};
font-size: ${vars.mediumFontSize};
padding: 5px;
border: 1px solid ${colors.darkGrey};
border: 1px solid ${theme.selectButtonBorder};
border-radius: 3px;
cursor: pointer;
overflow: hidden;
@@ -42,7 +42,8 @@ export const cssSelectBtn = styled('div', `
}
&.disabled {
color: grey;
--icon-color: ${theme.selectButtonDisabledFg};
color: ${theme.selectButtonDisabledFg};
cursor: pointer;
}
`);