diff --git a/app/client/aclui/ACLFormulaEditor.ts b/app/client/aclui/ACLFormulaEditor.ts index c2c6e338..f9c5e799 100644 --- a/app/client/aclui/ACLFormulaEditor.ts +++ b/app/client/aclui/ACLFormulaEditor.ts @@ -1,10 +1,13 @@ import {setupAceEditorCompletions} from 'app/client/components/AceEditorCompletions'; -import {colors} from 'app/client/ui2018/cssVars'; +import {theme} from 'app/client/ui2018/cssVars'; +import {Theme} from 'app/common/ThemePrefs'; +import {getGristConfig} from 'app/common/urlUtils'; import * as ace from 'brace'; -import {dom, DomArg, Observable, styled} from 'grainjs'; +import {Computed, dom, DomArg, Listener, Observable, styled} from 'grainjs'; import debounce from 'lodash/debounce'; export interface ACLFormulaOptions { + gristTheme: Computed; initialValue: string; readOnly: boolean; placeholder: DomArg; @@ -19,7 +22,19 @@ export function aclFormulaEditor(options: ACLFormulaOptions) { const editor: ace.Editor = ace.edit(editorElem); // Set various editor options. - editor.setTheme('ace/theme/chrome'); + function setAceTheme(gristTheme: Theme) { + const {enableCustomCss} = getGristConfig(); + const gristAppearance = gristTheme.appearance; + const aceTheme = gristAppearance === 'dark' && !enableCustomCss ? 'dracula' : 'chrome'; + editor.setTheme(`ace/theme/${aceTheme}`); + } + setAceTheme(options.gristTheme.get()); + let themeListener: Listener | undefined; + if (!getGristConfig().enableCustomCss) { + themeListener = options.gristTheme.addListener((gristTheme) => { + setAceTheme(gristTheme); + }); + } // ACE editor resizes automatically when maxLines is set. editor.setOptions({enableLiveAutocompletion: true, maxLines: 10}); editor.renderer.setShowGutter(false); // Default line numbers to hidden @@ -80,6 +95,7 @@ export function aclFormulaEditor(options: ACLFormulaOptions) { } return cssConditionInputAce( + dom.autoDispose(themeListener ?? null), cssConditionInputAce.cls('-disabled', options.readOnly), // ACE editor calls preventDefault on clicks into the scrollbar area, which prevents focus // being set when the click happens to be into there. To ensure we can focus on such clicks @@ -100,22 +116,25 @@ const cssConditionInputAce = styled('div', ` cursor: pointer; &:hover { - border: 1px solid ${colors.darkGrey}; + border: 1px solid ${theme.accessRulesFormulaEditorBorderHover}; } &:not(&-disabled):focus-within { - box-shadow: inset 0 0 0 1px ${colors.cursor}; - border-color: ${colors.cursor}; + box-shadow: inset 0 0 0 1px ${theme.accessRulesFormulaEditorFocus}; + border-color: ${theme.accessRulesFormulaEditorFocus}; } &:not(:focus-within) .ace_scroller, &-disabled .ace_scroller { cursor: unset; } &-disabled, &-disabled:hover { - background-color: ${colors.mediumGreyOpaque}; + background-color: ${theme.accessRulesFormulaEditorBgDisabled}; box-shadow: unset; border-color: transparent; } - &-disabled .ace-chrome { - background-color: ${colors.mediumGreyOpaque}; + & .ace-chrome, & .ace-dracula { + background-color: ${theme.accessRulesFormulaEditorBg}; + } + &-disabled .ace-chrome, &-disabled .ace-dracula { + background-color: ${theme.accessRulesFormulaEditorBgDisabled}; } & .ace_marker-layer, & .ace_cursor-layer { display: none; diff --git a/app/client/aclui/AccessRules.ts b/app/client/aclui/AccessRules.ts index 74bdf8f2..da544225 100644 --- a/app/client/aclui/AccessRules.ts +++ b/app/client/aclui/AccessRules.ts @@ -123,9 +123,9 @@ export class AccessRules extends Disposable { // Map of tableId to basic metadata for all tables in the document. private _aclResources = new Map(); - private _aclUsersPopup = ACLUsersPopup.create(this, this._gristDoc.docPageModel); + private _aclUsersPopup = ACLUsersPopup.create(this, this.gristDoc.docPageModel); - constructor(private _gristDoc: GristDoc) { + constructor(public gristDoc: GristDoc) { super(); this._ruleStatus = Computed.create(this, (use) => { const defRuleSet = use(this._docDefaultRuleSet); @@ -175,10 +175,10 @@ export class AccessRules extends Disposable { // changes). Instead, react deliberately if rules change. Note that table/column renames would // trigger changes to rules, so we don't need to listen for those separately. for (const tableId of ['_grist_ACLResources', '_grist_ACLRules']) { - const tableData = this._gristDoc.docData.getTable(tableId)!; + const tableData = this.gristDoc.docData.getTable(tableId)!; this.autoDispose(tableData.tableActionEmitter.addListener(this._onChange, this)); } - this.autoDispose(this._gristDoc.docPageModel.currentDoc.addListener(this._updateDocAccessData, this)); + this.autoDispose(this.gristDoc.docPageModel.currentDoc.addListener(this._updateDocAccessData, this)); this.update().catch((e) => this._errorMessage.set(e.message)); } @@ -202,9 +202,9 @@ export class AccessRules extends Disposable { const rules = this._ruleCollection; const [ , , aclResources] = await Promise.all([ - rules.update(this._gristDoc.docData, {log: console, pullOutSchemaEdit: true}), + rules.update(this.gristDoc.docData, {log: console, pullOutSchemaEdit: true}), this._updateDocAccessData(), - this._gristDoc.docComm.getAclResources(), + this.gristDoc.docComm.getAclResources(), ]); this._aclResources = new Map(Object.entries(aclResources.tables)); this._ruleProblems.set(aclResources.problems); @@ -244,7 +244,7 @@ export class AccessRules extends Disposable { // Note that if anything has changed, we apply changes relative to the current state of the // ACL tables (they may have changed by other users). So our changes will win. - const docData = this._gristDoc.docData; + const docData = this.gristDoc.docData; const resourcesTable = docData.getMetaTable('_grist_ACLResources'); const rulesTable = docData.getMetaTable('_grist_ACLRules'); @@ -346,7 +346,7 @@ export class AccessRules extends Disposable { public buildDom() { return cssOuter( - dom('div', this._gristDoc.behavioralPromptsManager.attachTip('accessRules', { + dom('div', this.gristDoc.behavioralPromptsManager.attachTip('accessRules', { hideArrow: true, })), cssAddTableRow( @@ -485,7 +485,7 @@ export class AccessRules extends Disposable { public async checkAclFormula(text: string): Promise { if (text) { - return this._gristDoc.docComm.checkAclFormula(text); + return this.gristDoc.docComm.checkAclFormula(text); } return {}; } @@ -1433,6 +1433,7 @@ class ObsUserAttributeRule extends Disposable { cssColumnGroup( cssCell1( aclFormulaEditor({ + gristTheme: this._accessRules.gristDoc.currentTheme, initialValue: this._charId.get(), readOnly: false, setValue: (text) => this._setUserAttr(text), @@ -1655,6 +1656,7 @@ class ObsRulePart extends Disposable { cssCell2( wide ? cssCell4.cls('') : null, aclFormulaEditor({ + gristTheme: this._ruleSet.accessRules.gristDoc.currentTheme, initialValue: this._aclFormula.get(), readOnly: this.isBuiltIn(), setValue: (value) => this._setAclFormula(value), diff --git a/app/client/components/AceEditor.css b/app/client/components/AceEditor.css index 5d26f55b..2aa78c1b 100644 --- a/app/client/components/AceEditor.css +++ b/app/client/components/AceEditor.css @@ -3,17 +3,21 @@ } .ace_grist_link { - color: var(--grist-color-light-green); + color: var(--grist-theme-ace-autocomplete-link, var(--grist-color-light-green)); text-decoration: underline; cursor: pointer; } .ace_grist_example { - color: #8f8f8f; + color: var(--grist-theme-ace-autocomplete-secondary-fg); +} + +.ace_editor.ace_autocomplete .ace_completion-highlight { + color: var(--grist-theme-ace-autocomplete-highlighted-fg, #000) !important; } .ace_editor.ace_autocomplete .ace_completion-highlight.ace_grist_link { - color: var(--grist-color-dark-green); + color: var(--grist-theme-ace-autocomplete-link-highlighted, var(--grist-color-dark-green)) !important; } .ace_editor.ace_autocomplete .ace_text-layer { @@ -22,6 +26,18 @@ } .ace_editor.ace_autocomplete { + color: var(--grist-theme-ace-autocomplete-primary-fg) !important; + background: var(--grist-theme-ace-autocomplete-bg, #fbfbfb) !important; + border: 1px solid var(--grist-theme-ace-autocomplete-border, lightgray) !important; width: 500px !important; /* the default in language_tools.js is 280px */ max-width: 80%; /* of the screen, for hypothetical mobile support */ } + +.ace_editor.ace_autocomplete .ace_marker-layer .ace_line-hover { + background-color: var(--grist-theme-ace-autocomplete-line-bg-hover, rgba(233,233,253,0.4)) !important; + border: 1px solid var(--grist-theme-ace-autocomplete-line-border-hover, #abbffe) !important; +} + +.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line { + background-color: var(--grist-theme-ace-autocomplete-active-line-bg, #CAD6FA) !important; +} diff --git a/app/client/components/AceEditor.js b/app/client/components/AceEditor.js index c17988be..8cb51a3c 100644 --- a/app/client/components/AceEditor.js +++ b/app/client/components/AceEditor.js @@ -204,7 +204,7 @@ AceEditor.prototype._setup = function() { this.session = this.editor.getSession(); this.session.setMode('ace/mode/python'); - const gristTheme = this.gristDoc?.docPageModel.appModel.currentTheme; + const gristTheme = this.gristDoc?.currentTheme; this._setAceTheme(gristTheme?.get()); if (!getGristConfig().enableCustomCss && gristTheme) { this.autoDispose(gristTheme.addListener((theme) => { diff --git a/app/client/components/ChartView.ts b/app/client/components/ChartView.ts index a0aa1e00..8fd99763 100644 --- a/app/client/components/ChartView.ts +++ b/app/client/components/ChartView.ts @@ -229,7 +229,7 @@ export class ChartView extends Disposable { this.listenTo(this.sortedRows, 'rowNotify', this._update); this.autoDispose(this.sortedRows.getKoArray().subscribe(this._update)); this.autoDispose(this._formatterComp.subscribe(this._update)); - this.autoDispose(this.gristDoc.docPageModel.appModel.currentTheme.addListener(() => this._update())); + this.autoDispose(this.gristDoc.currentTheme.addListener(() => this._update())); } public prepareToPrint(onOff: boolean) { diff --git a/app/client/components/CodeEditorPanel.css b/app/client/components/CodeEditorPanel.css index c946bbea..10d42fcf 100644 --- a/app/client/components/CodeEditorPanel.css +++ b/app/client/components/CodeEditorPanel.css @@ -50,3 +50,11 @@ .g-code-viewer .hljs-number { color: var(--grist-theme-code-view-number, #880000); } + +.g-code-viewer .hljs-built_in { + color: var(--grist-theme-code-view-builtin, #397300); +} + +.g-code-viewer .hljs-literal { + color: var(--grist-theme-code-view-literal, #78A960); +} diff --git a/app/client/components/GristDoc.ts b/app/client/components/GristDoc.ts index 589888bb..f6976f6d 100644 --- a/app/client/components/GristDoc.ts +++ b/app/client/components/GristDoc.ts @@ -182,6 +182,8 @@ export class GristDoc extends DisposableWithEvents { // Holder for the popped up formula editor. public readonly formulaPopup = Holder.create(this); + public readonly currentTheme = this.docPageModel.appModel.currentTheme; + private _actionLog: ActionLog; private _undoStack: UndoStack; private _lastOwnActionGroup: ActionGroupWithCursorPos|null = null; diff --git a/app/client/components/Importer.ts b/app/client/components/Importer.ts index 82ab4ae3..60df93b9 100644 --- a/app/client/components/Importer.ts +++ b/app/client/components/Importer.ts @@ -856,7 +856,8 @@ export class Importer extends DisposableWithEvents { return formula; }; - return cssFieldFormula(use => formatFormula(use(column.formula)), {placeholder, maxLines: 1}, + return cssFieldFormula(use => formatFormula(use(column.formula)), + {gristTheme: this._gristDoc.currentTheme, placeholder, maxLines: 1}, dom.cls('disabled'), {tabIndex: '-1'}, dom.on('focus', (_ev, elem) => buildEditor(elem)), diff --git a/app/client/components/Printing.ts b/app/client/components/Printing.ts index 9175bd03..3dccfff1 100644 --- a/app/client/components/Printing.ts +++ b/app/client/components/Printing.ts @@ -2,6 +2,7 @@ import {CustomView} from 'app/client/components/CustomView'; import {DataRowModel} from 'app/client/models/DataRowModel'; import DataTableModel from 'app/client/models/DataTableModel'; import {ViewSectionRec} from 'app/client/models/DocModel'; +import {prefersDarkMode, prefersDarkModeObs} from 'app/client/ui2018/cssVars'; import {dom} from 'grainjs'; type RowId = number|'new'; @@ -36,6 +37,16 @@ export async function printViewSection(layout: any, viewSection: ViewSectionRec) } function prepareToPrint(onOff: boolean) { + // window.print() is a blocking call, which means our listener for the + // `prefers-color-scheme: dark` media feature will not receive any updates for the + // duration that the print dialog is shown. This proves problematic since an event is + // sent just before the blocking call containing a value of false, regardless of the + // user agent's color scheme preference. It's not clear why this happens, but the result + // is Grist temporarily reverting to the light theme until the print dialog is dismissed. + // As a workaround, we'll temporarily pause our listener, and unpause after the print dialog + // is dismissed. + prefersDarkModeObs().pause(); + // Hide all layout boxes that do NOT contain the section to be printed. layout?.forEachBox((box: any) => { if (!box.dom.contains(sectionElem)) { @@ -76,6 +87,10 @@ export async function printViewSection(layout: any, viewSection: ViewSectionRec) prepareToPrint(false); } delete (window as any).afterPrintCallback; + prefersDarkModeObs().pause(false); + + // This may have changed while window.print() was blocking. + prefersDarkModeObs().set(prefersDarkMode()); }); // Running print on a timeout makes it possible to test printing using selenium, and doesn't diff --git a/app/client/lib/koForm.css b/app/client/lib/koForm.css index 9fa1d679..c85b00f5 100644 --- a/app/client/lib/koForm.css +++ b/app/client/lib/koForm.css @@ -108,7 +108,7 @@ div:hover > .kf_tooltip { box-shadow: 0 1px 1px 1px rgba(0,0,0,0.15); line-height: 1.1rem; font-size: 1rem; - color: #606060; + color: var(--grist-theme-prompt-fg, #606060); z-index: 10; } diff --git a/app/client/lib/pausableObs.ts b/app/client/lib/pausableObs.ts new file mode 100644 index 00000000..e3cfca9d --- /dev/null +++ b/app/client/lib/pausableObs.ts @@ -0,0 +1,41 @@ +import {IDisposableOwner, Observable} from 'grainjs'; + +export interface PausableObservable extends Observable { + pause(shouldPause?: boolean): void; +} + +/** + * Creates and returns an `Observable` that can be paused, effectively causing all + * calls to `set` to become noops until unpaused, at which point the last value + * passed to set, if any, will be applied. + * + * NOTE: It's only advisable to use this when there are no other alternatives; pausing + * updates and notifications to subscribers increases the chances of introducing bugs. + */ +export function createPausableObs( + owner: IDisposableOwner|null, + value: T, +): PausableObservable { + let _isPaused = false; + let _lastValue: T | undefined = undefined; + const obs = Observable.create(owner, value); + const set = Symbol('set'); + return Object.assign(obs, { + pause(shouldPause: boolean = true) { + _isPaused = shouldPause; + if (shouldPause) { + _lastValue = undefined; + } else if (_lastValue) { + obs.set(_lastValue); + _lastValue = undefined; + } + }, + [set]: obs.set, + set(val: T) { + _lastValue = val; + if (_isPaused) { return; } + + this[set](val); + } + }); +} diff --git a/app/client/models/UserPrefs.ts b/app/client/models/UserPrefs.ts index 94a0f3a3..3150ba33 100644 --- a/app/client/models/UserPrefs.ts +++ b/app/client/models/UserPrefs.ts @@ -20,18 +20,24 @@ function makePrefFunctions

(prefsTypeName: P) { */ function getPrefsObs(appModel: AppModel): Observable { if (appModel.currentValidUser) { - const prefsObs = Observable.create(null, appModel.currentOrg?.[prefsTypeName] ?? {}); + let prefs: PrefsType | undefined; + if (prefsTypeName === 'userPrefs') { + prefs = appModel.currentValidUser.prefs; + } else { + prefs = appModel.currentOrg?.[prefsTypeName]; + } + const prefsObs = Observable.create(null, prefs ?? {}); return Computed.create(null, (use) => use(prefsObs)) - .onWrite(prefs => { - prefsObs.set(prefs); - return appModel.api.updateOrg('current', {[prefsTypeName]: prefs}); + .onWrite(newPrefs => { + prefsObs.set(newPrefs); + return appModel.api.updateOrg('current', {[prefsTypeName]: newPrefs}); }); } else { const userId = appModel.currentUser?.id || 0; const jsonPrefsObs = localStorageObs(`${prefsTypeName}:u=${userId}`); return Computed.create(null, jsonPrefsObs, (use, p) => (p && JSON.parse(p) || {}) as PrefsType) - .onWrite(prefs => { - jsonPrefsObs.set(JSON.stringify(prefs)); + .onWrite(newPrefs => { + jsonPrefsObs.set(JSON.stringify(newPrefs)); }); } } diff --git a/app/client/ui/CodeHighlight.ts b/app/client/ui/CodeHighlight.ts index 63b24ddd..4dad60ab 100644 --- a/app/client/ui/CodeHighlight.ts +++ b/app/client/ui/CodeHighlight.ts @@ -1,13 +1,17 @@ -import {colors, theme, vars} from 'app/client/ui2018/cssVars'; +import {theme, vars} from 'app/client/ui2018/cssVars'; +import {Theme} from 'app/common/ThemePrefs'; +import {getGristConfig} from 'app/common/urlUtils'; import * as ace from 'brace'; -import {BindableValue, dom, DomElementArg, styled, subscribeElem} from 'grainjs'; +import {BindableValue, Computed, dom, DomElementArg, Observable, styled, subscribeElem} from 'grainjs'; // tslint:disable:no-var-requires require('brace/ext/static_highlight'); require("brace/mode/python"); require("brace/theme/chrome"); +require('brace/theme/dracula'); export interface ICodeOptions { + gristTheme: Computed; placeholder?: string; maxLines?: number; } @@ -15,29 +19,48 @@ export interface ICodeOptions { export function buildHighlightedCode( code: BindableValue, options: ICodeOptions, ...args: DomElementArg[] ): HTMLElement { + const {gristTheme, placeholder, maxLines} = options; + const {enableCustomCss} = getGristConfig(); + const highlighter = ace.acequire('ace/ext/static_highlight'); const PythonMode = ace.acequire('ace/mode/python').Mode; - const aceTheme = ace.acequire('ace/theme/chrome'); + const chrome = ace.acequire('ace/theme/chrome'); + const dracula = ace.acequire('ace/theme/dracula'); const mode = new PythonMode(); - return cssHighlightedCode( - dom('div', - elem => subscribeElem(elem, code, (codeText) => { - if (codeText) { - if (options.maxLines) { - // If requested, trim to maxLines, and add an ellipsis at the end. - // (Long lines are also truncated with an ellpsis via text-overflow style.) - const lines = codeText.split(/\n/); - if (lines.length > options.maxLines) { - codeText = lines.slice(0, options.maxLines).join("\n") + " \u2026"; // Ellipsis - } - } - elem.innerHTML = highlighter.render(codeText, mode, aceTheme, 1, true).html; - } else { - elem.textContent = options.placeholder || ''; + const codeText = Observable.create(null, ''); + const codeTheme = Observable.create(null, gristTheme.get()); + + function updateHighlightedCode(elem: HTMLElement) { + let text = codeText.get(); + if (text) { + if (maxLines) { + // If requested, trim to maxLines, and add an ellipsis at the end. + // (Long lines are also truncated with an ellpsis via text-overflow style.) + const lines = text.split(/\n/); + if (lines.length > maxLines) { + text = lines.slice(0, maxLines).join("\n") + " \u2026"; // Ellipsis } + } + + const aceTheme = codeTheme.get().appearance === 'dark' && !enableCustomCss ? dracula : chrome; + elem.innerHTML = highlighter.render(text, mode, aceTheme, 1, true).html; + } else { + elem.textContent = placeholder || ''; + } + } + + return cssHighlightedCode( + dom.autoDispose(codeText), + dom.autoDispose(codeTheme), + elem => subscribeElem(elem, code, (newCodeText) => { + codeText.set(newCodeText); + updateHighlightedCode(elem); }), - ), + elem => subscribeElem(elem, gristTheme, (newCodeTheme) => { + codeTheme.set(newCodeTheme); + updateHighlightedCode(elem); + }), ...args, ); } @@ -46,9 +69,9 @@ export function buildHighlightedCode( export const cssCodeBlock = styled('div', ` font-family: 'Monaco', 'Menlo', monospace; font-size: ${vars.smallFontSize}; - background-color: ${colors.light}; + background-color: ${theme.highlightedCodeBlockBg}; &[disabled], &.disabled { - background-color: ${colors.mediumGreyOpaque}; + background-color: ${theme.highlightedCodeBlockBgDisabled}; } `); @@ -57,14 +80,14 @@ const cssHighlightedCode = styled(cssCodeBlock, ` white-space: pre; overflow: hidden; text-overflow: ellipsis; - border: 1px solid ${colors.darkGrey}; + border: 1px solid ${theme.highlightedCodeBorder}; border-radius: 3px; min-height: 28px; padding: 5px 6px; - color: ${colors.slate}; + color: ${theme.highlightedCodeFg}; - &.disabled, &.disabled .ace-chrome { - background-color: ${colors.mediumGreyOpaque}; + &.disabled, &.disabled .ace-chrome, &.disabled .ace-dracula { + background-color: ${theme.highlightedCodeBgDisabled}; } & .ace_line { overflow: hidden; @@ -80,7 +103,7 @@ export const cssFieldFormula = styled(buildHighlightedCode, ` --icon-color: ${theme.accentIcon}; &-disabled-icon.formula_field_sidepane::before { - --icon-color: ${theme.lightText}; + --icon-color: ${theme.iconDisabled}; } &-disabled { pointer-events: none; diff --git a/app/client/ui/CustomSectionConfig.ts b/app/client/ui/CustomSectionConfig.ts index d5c894e4..59d66e3e 100644 --- a/app/client/ui/CustomSectionConfig.ts +++ b/app/client/ui/CustomSectionConfig.ts @@ -7,7 +7,7 @@ import {reportError} from 'app/client/models/errors'; import {cssHelp, cssLabel, cssRow, cssSeparator} from 'app/client/ui/RightPanelStyles'; import {cssDragRow, cssFieldEntry, cssFieldLabel} from 'app/client/ui/VisibleFieldsConfig'; import {basicButton, primaryButton, textButton} from 'app/client/ui2018/buttons'; -import {colors, vars} from 'app/client/ui2018/cssVars'; +import {theme, vars} from 'app/client/ui2018/cssVars'; import {cssDragger} from 'app/client/ui2018/draggableList'; import {textInput} from 'app/client/ui2018/editableLabel'; import {IconName} from 'app/client/ui2018/IconList'; @@ -511,7 +511,7 @@ export class CustomSectionConfig extends Disposable { const cssWarningWrapper = styled('div', ` padding-left: 8px; padding-top: 6px; - --icon-color: ${colors.error} + --icon-color: ${theme.iconError} `); const cssColumns = styled('div', ` @@ -534,7 +534,7 @@ const cssSection = styled('div', ` const cssMenu = styled('div', ` & > li:first-child { - border-bottom: 1px solid ${colors.mediumGrey}; + border-bottom: 1px solid ${theme.menuBorder}; } `); @@ -556,30 +556,37 @@ const cssRemoveIcon = styled(icon, ` const cssSubLabel = styled('span', ` text-transform: none; font-size: ${vars.xsmallFontSize}; - color: ${colors.slate}; + color: ${theme.lightText}; `); const cssAddMapping = styled('div', ` display: flex; cursor: pointer; - color: ${colors.lightGreen}; - --icon-color: ${colors.lightGreen}; + color: ${theme.controlFg}; + --icon-color: ${theme.controlFg}; &:not(:first-child) { margin-top: 8px; } &:hover, &:focus, &:active { - color: ${colors.darkGreen}; - --icon-color: ${colors.darkGreen}; + color: ${theme.controlHoverFg}; + --icon-color: ${theme.controlHoverFg}; } `); const cssTextInput = styled(textInput, ` flex: 1 0 auto; + color: ${theme.inputFg}; + background-color: ${theme.inputBg}; + &:disabled { - color: ${colors.slate}; - background-color: ${colors.lightGrey}; + color: ${theme.inputDisabledFg}; + background-color: ${theme.inputDisabledBg}; pointer-events: none; } + + &::placeholder { + color: ${theme.inputPlaceholderFg}; + } `); diff --git a/app/client/ui/FieldConfig.ts b/app/client/ui/FieldConfig.ts index c128d583..903fc78b 100644 --- a/app/client/ui/FieldConfig.ts +++ b/app/client/ui/FieldConfig.ts @@ -17,6 +17,7 @@ import {GRIST_FORMULA_ASSISTANT} from 'app/client/models/features'; import {selectMenu, selectOption, selectTitle} from 'app/client/ui2018/menus'; import {createFormulaErrorObs, cssError} from 'app/client/widgets/FormulaEditor'; import {sanitizeIdent} from 'app/common/gutil'; +import {Theme} from 'app/common/ThemePrefs'; import {bundleChanges, Computed, dom, DomContents, DomElementArg, fromKo, MultiHolder, Observable, styled} from 'grainjs'; import * as ko from 'knockout'; @@ -316,10 +317,13 @@ export function buildFormulaConfig( cssRow(formulaField = buildFormula( origColumn, buildEditor, - t("Enter formula"), - disableOtherActions, - onSave, - clearState)), + { + gristTheme: gristDoc.currentTheme, + placeholder: t("Enter formula"), + disabled: disableOtherActions, + onSave, + onCancel: clearState, + })), dom.maybe(errorMessage, errMsg => cssRow(cssError(errMsg), testId('field-error-count'))), ]; @@ -404,14 +408,21 @@ export function buildFormulaConfig( ]); } +interface BuildFormulaOptions { + gristTheme: Computed; + placeholder: string; + disabled: Observable; + onSave?: SaveHandler; + onCancel?: () => void; +} + function buildFormula( - column: ColumnRec, - buildEditor: BuildEditor, - placeholder: string, - disabled: Observable, - onSave?: SaveHandler, - onCancel?: () => void) { - return cssFieldFormula(column.formula, {placeholder, maxLines: 2}, + column: ColumnRec, + buildEditor: BuildEditor, + options: BuildFormulaOptions +) { + const {gristTheme, placeholder, disabled, onSave, onCancel} = options; + return cssFieldFormula(column.formula, {gristTheme, placeholder, maxLines: 2}, dom.cls('formula_field_sidepane'), cssFieldFormula.cls('-disabled', disabled), cssFieldFormula.cls('-disabled-icon', use => !use(column.formula)), diff --git a/app/client/ui/FormulaAssistance.ts b/app/client/ui/FormulaAssistance.ts index aa1de920..1d3efe32 100644 --- a/app/client/ui/FormulaAssistance.ts +++ b/app/client/ui/FormulaAssistance.ts @@ -236,7 +236,10 @@ function buildChat(owner: Disposable, context: Context & { formulaClicked: (form } else { return cssAiMessage( cssAvatar(cssAiImage()), - buildHighlightedCode(entry.message, { maxLines: 10 }, cssCodeStyles.cls('')), + buildHighlightedCode(entry.message, { + gristTheme: grist.currentTheme, + maxLines: 10, + }, cssCodeStyles.cls('')), cssCopyIconWrapper( icon('Copy', dom.on('click', () => context.formulaClicked(entry.message))), ) diff --git a/app/client/ui2018/ColorSelect.ts b/app/client/ui2018/ColorSelect.ts index a473fed7..0baa2382 100644 --- a/app/client/ui2018/ColorSelect.ts +++ b/app/client/ui2018/ColorSelect.ts @@ -1,6 +1,6 @@ import {basicButton, primaryButton} from 'app/client/ui2018/buttons'; import {isLight, swatches} from 'app/client/ui2018/ColorPalette'; -import {colors, testId, theme, vars} from 'app/client/ui2018/cssVars'; +import {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'; @@ -366,24 +366,24 @@ class FontComponent extends Disposable { const cssFontOptions = styled('div', ` display: flex; gap: 1px; - background: ${colors.darkGrey}; - border: 1px solid ${colors.darkGrey}; + background: ${theme.colorSelectFontOptionsBorder}; + border: 1px solid ${theme.colorSelectFontOptionsBorder}; `); const cssFontOption = styled('div', ` display: grid; place-items: center; flex-grow: 1; - background: ${colors.light}; - --icon-color: ${colors.dark}; + background: ${theme.colorSelectFontOptionBg}; + --icon-color: ${theme.colorSelectFontOptionFg}; height: 24px; cursor: pointer; &:hover:not(&-selected) { - background: ${colors.lightGrey}; + background: ${theme.colorSelectFontOptionBgHover}; } &-selected { - background: ${colors.dark}; - --icon-color: ${colors.light} + background: ${theme.colorSelectFontOptionBgSelected}; + --icon-color: ${theme.colorSelectFontOptionFgSelected} } `); @@ -409,6 +409,7 @@ const cssControlRow = styled('div', ` `); const cssHeaderRow = styled('div', ` + color: ${theme.colorSelectFg}; text-transform: uppercase; font-size: ${vars.smallFontSize}; margin-bottom: 12px; @@ -430,8 +431,8 @@ const cssVSpacer = styled('div', ` const cssContainer = styled('div', ` padding: 18px 16px; - background-color: white; - box-shadow: 0 2px 16px 0 rgba(38,38,51,0.6); + background-color: ${theme.colorSelectBg}; + box-shadow: 0 2px 16px 0 ${theme.colorSelectShadow}; z-index: 20; margin: 2px 0; &:focus { @@ -445,13 +446,13 @@ const cssContent = styled('div', ` `); const cssHexBox = styled(textInput, ` - border: 1px solid ${theme.inputBorder}; + border: 1px solid ${theme.colorSelectInputBorder}; border-left: none; font-size: ${vars.smallFontSize}; display: flex; align-items: center; - color: ${theme.lightText}; - background-color: ${theme.inputBg}; + color: ${theme.colorSelectInputFg}; + background-color: ${theme.colorSelectInputBg}; width: 56px; outline: none; padding: 0 3px; @@ -460,7 +461,7 @@ const cssHexBox = styled(textInput, ` `); const cssLightBorder = styled('div', ` - border: 1px solid #D9D9D9; + border: 1px solid ${theme.colorSelectColorSquareBorder}; `); const cssColorSquare = styled('div', ` @@ -471,16 +472,16 @@ const cssColorSquare = styled('div', ` align-items: center; position: relative; &-selected { - outline: 1px solid #D9D9D9; + outline: 1px solid ${theme.colorSelectColorSquareBorder}; outline-offset: 1px; } `); const cssEmptyBox = styled(cssColorSquare, ` - --icon-color: ${colors.error}; + --icon-color: ${theme.iconError}; border: 1px solid #D9D9D9; &-selected { - outline: 1px solid ${colors.dark}; + outline: 1px solid ${theme.colorSelectColorSquareBorderEmpty}; outline-offset: 1px; } `); @@ -493,7 +494,7 @@ const cssFontIcon = styled(icon, ` const cssNoneIcon = styled(icon, ` height: 100%; width: 100%; - --icon-color: ${colors.error} + --icon-color: ${theme.iconError} `); const cssButtonIcon = styled(cssColorSquare, ` diff --git a/app/client/ui2018/cssVars.ts b/app/client/ui2018/cssVars.ts index 03f15f07..3958fef3 100644 --- a/app/client/ui2018/cssVars.ts +++ b/app/client/ui2018/cssVars.ts @@ -6,6 +6,7 @@ * https://css-tricks.com/snippets/css/system-font-stack/ * */ +import {createPausableObs, PausableObservable} from 'app/client/lib/pausableObs'; import {getStorage} from 'app/client/lib/storage'; import {urlState} from 'app/client/models/gristUrlState'; import {getTheme, ProductFlavor} from 'app/client/ui/CustomThemes'; @@ -257,6 +258,9 @@ export const theme = { 'rgba(76, 86, 103, 0.24)'), popupCloseButtonFg: new CustomProp('theme-popup-close-button-fg', undefined, colors.slate), + /* Prompts */ + promptFg: new CustomProp('theme-prompt-fg', undefined, '#606060'), + /* Progress Bars */ progressBarFg: new CustomProp('theme-progress-bar-fg', undefined, colors.lightGreen), progressBarErrorFg: new CustomProp('theme-progress-bar-error-fg', undefined, colors.error), @@ -382,6 +386,10 @@ export const theme = { filterBarButtonSavedHoverBg: new CustomProp('theme-filter-bar-button-saved-hover-bg', undefined, colors.darkGrey), + /* Icons */ + iconDisabled: new CustomProp('theme-icon-disabled', undefined, colors.slate), + iconError: new CustomProp('theme-icon-error', undefined, colors.error), + /* Icon Buttons */ iconButtonFg: new CustomProp('theme-icon-button-fg', undefined, colors.light), iconButtonPrimaryBg: new CustomProp('theme-icon-button-primary-bg', undefined, @@ -589,6 +597,8 @@ export const theme = { 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'), + codeViewBuiltin: new CustomProp('theme-code-view-builtin', undefined, '#397300'), + codeViewLiteral: new CustomProp('theme-code-view-literal', undefined, '#78A960'), /* Importer */ importerTableInfoBorder: new CustomProp('theme-importer-table-info-border', undefined, colors.darkGrey), @@ -646,6 +656,14 @@ export const theme = { undefined, colors.light), accessRulesColumnItemIconHoverBg: new CustomProp('theme-access-rules-column-item-icon-hover-bg', undefined, colors.slate), + accessRulesFormulaEditorBg: new CustomProp('theme-access-rules-formula-editor-bg', undefined, + 'white'), + accessRulesFormulaEditorBorderHover: new CustomProp( + 'theme-access-rules-formula-editor-border-hover', undefined, colors.darkGrey), + accessRulesFormulaEditorBgDisabled: new CustomProp( + 'theme-access-rules-formula-editor-bg-disabled', undefined, colors.mediumGreyOpaque), + accessRulesFormulaEditorFocus: new CustomProp('theme-access-rules-formula-editor-focus', + undefined, colors.cursor), /* Cells */ cellFg: new CustomProp('theme-cell-fg', undefined, 'unset'), @@ -701,6 +719,63 @@ export const theme = { tutorialsPopupHeaderFg: new CustomProp('theme-tutorials-popup-header-fg', undefined, colors.lightGreen), tutorialsPopupBoxBg: new CustomProp('theme-tutorials-popup-box-bg', undefined, '#F5F5F5'), + + /* Ace Autocomplete */ + aceAutocompletePrimaryFg: new CustomProp('theme-ace-autocomplete-primary-fg', undefined, '#444'), + aceAutocompleteSecondaryFg: new CustomProp('theme-ace-autocomplete-secondary-fg', undefined, + '#8f8f8f'), + aceAutocompleteHighlightedFg: new CustomProp('theme-ace-autocomplete-highlighted-fg', undefined, '#000'), + aceAutocompleteBg: new CustomProp('theme-ace-autocomplete-bg', undefined, '#FBFBFB'), + aceAutocompleteBorder: new CustomProp('theme-ace-autocomplete-border', undefined, 'lightgray'), + aceAutocompleteLink: new CustomProp('theme-ace-autocomplete-link', undefined, colors.lightGreen), + aceAutocompleteLinkHighlighted: new CustomProp('theme-ace-autocomplete-link-highlighted', + undefined, colors.darkGreen), + aceAutocompleteActiveLineBg: new CustomProp('theme-ace-autocomplete-active-line-bg', + undefined, '#CAD6FA'), + aceAutocompleteLineBorderHover: new CustomProp('theme-ace-autocomplete-line-border-hover', + undefined, '#abbffe'), + aceAutocompleteLineBgHover: new CustomProp('theme-ace-autocomplete-line-bg-hover', + undefined, 'rgba(233,233,253,0.4)'), + + /* Color Select */ + colorSelectFg: new CustomProp('theme-color-select-fg', undefined, colors.dark), + colorSelectBg: new CustomProp('theme-color-select-bg', undefined, 'white'), + colorSelectShadow: new CustomProp('theme-color-select-shadow', undefined, + 'rgba(38,38,51,0.6)'), + colorSelectFontOptionsBorder: new CustomProp('theme-color-select-font-options-border', + undefined, colors.darkGrey), + colorSelectFontOptionFg: new CustomProp('theme-color-select-font-option-fg', + undefined, colors.dark), + colorSelectFontOptionBg: new CustomProp('theme-color-select-font-option-bg', + undefined, colors.light), + colorSelectFontOptionBgHover: new CustomProp('theme-color-select-font-option-bg-hover', + undefined, colors.lightGrey), + colorSelectFontOptionFgSelected: new CustomProp('theme-color-select-font-option-selected-fg', + undefined, colors.light), + colorSelectFontOptionBgSelected: new CustomProp('theme-color-select-font-option-selected-bg', + undefined, colors.dark), + colorSelectColorSquareBorder: new CustomProp('theme-color-select-color-square-border', + undefined, '#D9D9D9'), + colorSelectColorSquareBorderEmpty: new CustomProp('theme-color-select-color-square-border-empty', + undefined, colors.dark), + colorSelectInputFg: new CustomProp('theme-color-select-input-fg', + undefined, colors.slate), + colorSelectInputBg: new CustomProp('theme-color-select-input-bg', + undefined, 'white'), + colorSelectInputBorder: new CustomProp('theme-color-select-input-border', + undefined, colors.darkGrey), + + /* Highlighted Code */ + highlightedCodeBlockBg: new CustomProp('theme-highlighted-code-block-bg', undefined, + colors.light), + highlightedCodeBlockBgDisabled: new CustomProp('theme-highlighted-code-block-bg-disabled', + undefined, colors.mediumGreyOpaque), + highlightedCodeFg: new CustomProp('theme-highlighted-code-fg', + undefined, colors.slate), + highlightedCodeBorder: new CustomProp('theme-highlighted-code-border', + undefined, colors.darkGrey), + highlightedCodeBgDisabled: new CustomProp('theme-highlighted-code-bg-disabled', + undefined, colors.mediumGreyOpaque), }; const cssColors = values(colors).map(v => v.decl()).join('\n'); @@ -823,15 +898,19 @@ export function isScreenResizing(): Observable { return _isScreenResizingObs; } -let _prefersDarkModeObs: Observable|undefined; +export function prefersDarkMode(): boolean { + return window.matchMedia('(prefers-color-scheme: dark)').matches; +} + +let _prefersDarkModeObs: PausableObservable|undefined; /** * Returns a singleton observable for whether the user agent prefers dark mode. */ -export function prefersDarkModeObs(): Observable { +export function prefersDarkModeObs(): PausableObservable { if (!_prefersDarkModeObs) { const query = window.matchMedia('(prefers-color-scheme: dark)'); - const obs = Observable.create(null, query.matches); + const obs = createPausableObs(null, query.matches); query.addEventListener('change', event => obs.set(event.matches)); _prefersDarkModeObs = obs; } diff --git a/app/client/widgets/ConditionalStyle.ts b/app/client/widgets/ConditionalStyle.ts index 7ae0f87c..44a79bdf 100644 --- a/app/client/widgets/ConditionalStyle.ts +++ b/app/client/widgets/ConditionalStyle.ts @@ -186,7 +186,7 @@ export class ConditionalStyle extends Disposable { ) { return cssFieldFormula( formula, - { maxLines: 1 }, + { gristTheme: this._gristDoc.currentTheme, maxLines: 1 }, dom.cls('formula_field_sidepane'), dom.cls(cssErrorBorder.className, hasError), { tabIndex: '-1' }, diff --git a/app/common/LoginSessionAPI.ts b/app/common/LoginSessionAPI.ts index 23fb50b4..959f51c9 100644 --- a/app/common/LoginSessionAPI.ts +++ b/app/common/LoginSessionAPI.ts @@ -1,3 +1,5 @@ +import {UserPrefs} from 'app/common/Prefs'; + // User profile info for the user. When using Cognito, it is fetched during login. export interface UserProfile { email: string; @@ -16,6 +18,7 @@ export interface FullUser extends UserProfile { ref?: string|null; // Not filled for anonymous users. allowGoogleLogin?: boolean; // when present, specifies whether logging in via Google is possible. isSupport?: boolean; // set if user is a special support user. + prefs?: UserPrefs; } export interface LoginSessionAPI { diff --git a/app/common/ThemePrefs-ti.ts b/app/common/ThemePrefs-ti.ts index 14f3df00..0eb48028 100644 --- a/app/common/ThemePrefs-ti.ts +++ b/app/common/ThemePrefs-ti.ts @@ -93,6 +93,7 @@ export const ThemeColors = t.iface([], { "popup-shadow-inner": "string", "popup-shadow-outer": "string", "popup-close-button-fg": "string", + "prompt-fg": "string", "progress-bar-fg": "string", "progress-bar-error-fg": "string", "progress-bar-bg": "string", @@ -167,6 +168,8 @@ export const ThemeColors = t.iface([], { "filter-bar-button-saved-fg": "string", "filter-bar-button-saved-bg": "string", "filter-bar-button-saved-hover-bg": "string", + "icon-disabled": "string", + "icon-error": "string", "icon-button-fg": "string", "icon-button-primary-bg": "string", "icon-button-primary-hover-bg": "string", @@ -287,6 +290,8 @@ export const ThemeColors = t.iface([], { "code-view-params": "string", "code-view-string": "string", "code-view-number": "string", + "code-view-builtin": "string", + "code-view-literal": "string", "importer-table-info-border": "string", "importer-preview-border": "string", "importer-skipped-table-overlay": "string", @@ -319,6 +324,10 @@ export const ThemeColors = t.iface([], { "access-rules-column-item-icon-fg": "string", "access-rules-column-item-icon-hover-fg": "string", "access-rules-column-item-icon-hover-bg": "string", + "access-rules-formula-editor-bg": "string", + "access-rules-formula-editor-border-hover": "string", + "access-rules-formula-editor-bg-disabled": "string", + "access-rules-formula-editor-focus": "string", "cell-fg": "string", "cell-bg": "string", "cell-zebra-bg": "string", @@ -348,6 +357,35 @@ export const ThemeColors = t.iface([], { "tutorials-popup-border": "string", "tutorials-popup-header-fg": "string", "tutorials-popup-box-bg": "string", + "ace-autocomplete-primary-fg": "string", + "ace-autocomplete-secondary-fg": "string", + "ace-autocomplete-highlighted-fg": "string", + "ace-autocomplete-bg": "string", + "ace-autocomplete-border": "string", + "ace-autocomplete-link": "string", + "ace-autocomplete-link-highlighted": "string", + "ace-autocomplete-active-line-bg": "string", + "ace-autocomplete-line-border-hover": "string", + "ace-autocomplete-line-bg-hover": "string", + "color-select-fg": "string", + "color-select-bg": "string", + "color-select-shadow": "string", + "color-select-font-options-border": "string", + "color-select-font-option-fg": "string", + "color-select-font-option-bg": "string", + "color-select-font-option-bg-hover": "string", + "color-select-font-option-fg-selected": "string", + "color-select-font-option-bg-selected": "string", + "color-select-color-square-border": "string", + "color-select-color-square-border-empty": "string", + "color-select-input-fg": "string", + "color-select-input-bg": "string", + "color-select-input-border": "string", + "highlighted-code-block-bg": "string", + "highlighted-code-block-bg-disabled": "string", + "highlighted-code-fg": "string", + "highlighted-code-border": "string", + "highlighted-code-bg-disabled": "string", }); const exportedTypeSuite: t.ITypeSuite = { diff --git a/app/common/ThemePrefs.ts b/app/common/ThemePrefs.ts index ff6825fd..af4f4292 100644 --- a/app/common/ThemePrefs.ts +++ b/app/common/ThemePrefs.ts @@ -110,6 +110,9 @@ export interface ThemeColors { 'popup-shadow-outer': string; 'popup-close-button-fg': string; + /* Prompts */ + 'prompt-fg': string; + /* Progress Bars */ 'progress-bar-fg': string; 'progress-bar-error-fg': string; @@ -216,6 +219,10 @@ export interface ThemeColors { 'filter-bar-button-saved-bg': string; 'filter-bar-button-saved-hover-bg': string; + /* Icons */ + 'icon-disabled': string; + 'icon-error': string; + /* Icon Buttons */ 'icon-button-fg': string; 'icon-button-primary-bg': string; @@ -373,6 +380,8 @@ export interface ThemeColors { 'code-view-params': string; 'code-view-string': string; 'code-view-number': string; + 'code-view-builtin': string; + 'code-view-literal': string; /* Importer */ 'importer-table-info-border': string; @@ -415,6 +424,10 @@ export interface ThemeColors { 'access-rules-column-item-icon-fg': string; 'access-rules-column-item-icon-hover-fg': string; 'access-rules-column-item-icon-hover-bg': string; + 'access-rules-formula-editor-bg': string; + 'access-rules-formula-editor-border-hover': string; + 'access-rules-formula-editor-bg-disabled': string; + 'access-rules-formula-editor-focus': string; /* Cells */ 'cell-fg': string; @@ -456,6 +469,41 @@ export interface ThemeColors { 'tutorials-popup-border': string; 'tutorials-popup-header-fg': string; 'tutorials-popup-box-bg': string; + + /* Ace Autocomplete */ + 'ace-autocomplete-primary-fg': string; + 'ace-autocomplete-secondary-fg': string; + 'ace-autocomplete-highlighted-fg': string; + 'ace-autocomplete-bg': string; + 'ace-autocomplete-border': string; + 'ace-autocomplete-link': string; + 'ace-autocomplete-link-highlighted': string; + 'ace-autocomplete-active-line-bg': string; + 'ace-autocomplete-line-border-hover': string; + 'ace-autocomplete-line-bg-hover': string; + + /* Color Select */ + 'color-select-fg': string; + 'color-select-bg': string; + 'color-select-shadow': string; + 'color-select-font-options-border': string; + 'color-select-font-option-fg': string; + 'color-select-font-option-bg': string; + 'color-select-font-option-bg-hover': string; + 'color-select-font-option-fg-selected': string; + 'color-select-font-option-bg-selected': string; + 'color-select-color-square-border': string; + 'color-select-color-square-border-empty': string; + 'color-select-input-fg': string; + 'color-select-input-bg': string; + 'color-select-input-border': string; + + /* Highlighted Code */ + 'highlighted-code-block-bg': string; + 'highlighted-code-block-bg-disabled': string; + 'highlighted-code-fg': string; + 'highlighted-code-border': string; + 'highlighted-code-bg-disabled': string; } export const ThemePrefsChecker = createCheckers(ThemePrefsTI).ThemePrefs as CheckerT; diff --git a/app/common/themes/GristDark.ts b/app/common/themes/GristDark.ts index 8ef30e75..8b960e7d 100644 --- a/app/common/themes/GristDark.ts +++ b/app/common/themes/GristDark.ts @@ -89,6 +89,9 @@ export const GristDark: ThemeColors = { 'popup-shadow-outer': '#000000', 'popup-close-button-fg': '#A4A4A4', + /* Prompts */ + 'prompt-fg': '#A4A4A4', + /* Progress Bars */ 'progress-bar-fg': '#1DA270', 'progress-bar-error-fg': '#FF6666', @@ -195,6 +198,10 @@ export const GristDark: ThemeColors = { 'filter-bar-button-saved-bg': '#555563', 'filter-bar-button-saved-hover-bg': '#69697D', + /* Icons */ + 'icon-disabled': '#A4A4A4', + 'icon-error': '#FFA500', + /* Icon Buttons */ 'icon-button-fg': '#FFFFFF', 'icon-button-primary-bg': '#1DA270', @@ -352,6 +359,8 @@ export const GristDark: ThemeColors = { 'code-view-params': '#D2D2D2', 'code-view-string': '#ED7373', 'code-view-number': '#ED7373', + 'code-view-builtin': '#BFE6D8', + 'code-view-literal': '#9ED682', /* Importer */ 'importer-table-info-border': '#69697D', @@ -394,6 +403,10 @@ export const GristDark: ThemeColors = { 'access-rules-column-item-icon-fg': '#A4A4A4', 'access-rules-column-item-icon-hover-fg': '#EFEFEF', 'access-rules-column-item-icon-hover-bg': '#A4A4A4', + 'access-rules-formula-editor-bg': '#32323F', + 'access-rules-formula-editor-border-hover': '#69697D', + 'access-rules-formula-editor-bg-disabled': '#57575F', + 'access-rules-formula-editor-focus': '#1DA270', /* Cells */ 'cell-fg': '#FFFFFF', @@ -435,4 +448,39 @@ export const GristDark: ThemeColors = { 'tutorials-popup-border': '#69697D', 'tutorials-popup-header-fg': '#FFFFFF', 'tutorials-popup-box-bg': '#57575F', + + /* Ace Autocomplete */ + 'ace-autocomplete-primary-fg': '#EFEFEF', + 'ace-autocomplete-secondary-fg': '#A4A4A4', + 'ace-autocomplete-highlighted-fg': '#FFFFFF', + 'ace-autocomplete-bg': '#32323F', + 'ace-autocomplete-border': '#69697D', + 'ace-autocomplete-link': '#28BE86', + 'ace-autocomplete-link-highlighted': '#45D48B', + 'ace-autocomplete-active-line-bg': '#555563', + 'ace-autocomplete-line-border-hover': 'rgba(111,111,117,0.3)', + 'ace-autocomplete-line-bg-hover': 'rgba(111,111,117,0.3)', + + /* Color Select */ + 'color-select-fg': '#EFEFEF', + 'color-select-bg': '#32323F', + 'color-select-shadow': '#000000', + 'color-select-font-options-border': '#69697D', + 'color-select-font-option-fg': '#EFEFEF', + 'color-select-font-option-bg': '#32323F', + 'color-select-font-option-bg-hover': '#262633', + 'color-select-font-option-fg-selected': '#EFEFEF', + 'color-select-font-option-bg-selected': '#555563', + 'color-select-color-square-border': '#A4A4A4', + 'color-select-color-square-border-empty': '#EFEFEF', + 'color-select-input-fg': '#EFEFEF', + 'color-select-input-bg': '#32323F', + 'color-select-input-border': '#69697D', + + /* Highlighted Code */ + 'highlighted-code-block-bg': '#262633', + 'highlighted-code-block-bg-disabled': '#555563', + 'highlighted-code-fg': '#A4A4A4', + 'highlighted-code-border': '#69697D', + 'highlighted-code-bg-disabled': '#555563', }; diff --git a/app/common/themes/GristLight.ts b/app/common/themes/GristLight.ts index be19657b..d9b75599 100644 --- a/app/common/themes/GristLight.ts +++ b/app/common/themes/GristLight.ts @@ -89,6 +89,9 @@ export const GristLight: ThemeColors = { 'popup-shadow-outer': 'rgba(76, 86, 103, 0.24)', 'popup-close-button-fg': '#929299', + /* Prompts */ + 'prompt-fg': '#606060', + /* Progress Bars */ 'progress-bar-fg': '#16B378', 'progress-bar-error-fg': '#D0021B', @@ -195,6 +198,10 @@ export const GristLight: ThemeColors = { 'filter-bar-button-saved-bg': '#929299', 'filter-bar-button-saved-hover-bg': '#D9D9D9', + /* Icons */ + 'icon-disabled': '#929299', + 'icon-error': '#D0021B', + /* Icon Buttons */ 'icon-button-fg': '#FFFFFF', 'icon-button-primary-bg': '#16B378', @@ -352,6 +359,8 @@ export const GristLight: ThemeColors = { 'code-view-params': '#444', 'code-view-string': '#880000', 'code-view-number': '#880000', + 'code-view-builtin': '#397300', + 'code-view-literal': '#78A960', /* Importer */ 'importer-table-info-border': '#D9D9D9', @@ -394,6 +403,10 @@ export const GristLight: ThemeColors = { 'access-rules-column-item-icon-fg': '#929299', 'access-rules-column-item-icon-hover-fg': '#FFFFFF', 'access-rules-column-item-icon-hover-bg': '#929299', + 'access-rules-formula-editor-bg': 'white', + 'access-rules-formula-editor-border-hover': '#D9D9D9', + 'access-rules-formula-editor-bg-disabled': '#E8E8E8', + 'access-rules-formula-editor-focus': '#16B378', /* Cells */ 'cell-fg': 'black', @@ -435,4 +448,39 @@ export const GristLight: ThemeColors = { 'tutorials-popup-border': '#D9D9D9', 'tutorials-popup-header-fg': '#FFFFFF', 'tutorials-popup-box-bg': '#F5F5F5', + + /* Ace Autocomplete */ + 'ace-autocomplete-primary-fg': '#444', + 'ace-autocomplete-secondary-fg': '#8F8F8F', + 'ace-autocomplete-highlighted-fg': '#000', + 'ace-autocomplete-bg': '#FBFBFB', + 'ace-autocomplete-border': 'lightgray', + 'ace-autocomplete-link': '#16B378', + 'ace-autocomplete-link-highlighted': '#009058', + 'ace-autocomplete-active-line-bg': '#CAD6FA', + 'ace-autocomplete-line-border-hover': '#ABBFFE', + 'ace-autocomplete-line-bg-hover': 'rgba(233,233,253,0.4)', + + /* Color Select */ + 'color-select-fg': '#262633', + 'color-select-bg': 'white', + 'color-select-shadow': 'rgba(38,38,51,0.6)', + 'color-select-font-options-border': '#D9D9D9', + 'color-select-font-option-fg': '#262633', + 'color-select-font-option-bg': '#FFFFFF', + 'color-select-font-option-bg-hover': '#F7F7F7', + 'color-select-font-option-fg-selected': '#FFFFFF', + 'color-select-font-option-bg-selected': '#262633', + 'color-select-color-square-border': '#D9D9D9', + 'color-select-color-square-border-empty': '#262633', + 'color-select-input-fg': '#929299', + 'color-select-input-bg': 'white', + 'color-select-input-border': '#D9D9D9', + + /* Highlighted Code */ + 'highlighted-code-block-bg': '#FFFFFF', + 'highlighted-code-block-bg-disabled': '#E8E8E8', + 'highlighted-code-fg': '#929299', + 'highlighted-code-border': '#D9D9D9', + 'highlighted-code-bg-disabled': '#E8E8E8', }; diff --git a/app/gen-server/ApiServer.ts b/app/gen-server/ApiServer.ts index 2aeaf2e2..736fdc43 100644 --- a/app/gen-server/ApiServer.ts +++ b/app/gen-server/ApiServer.ts @@ -472,7 +472,7 @@ export class ApiServer { // GET /api/session/access/active // Returns active user and active org (if any) this._app.get('/api/session/access/active', expressWrap(async (req, res) => { - const fullUser = await this._getFullUser(req); + const fullUser = await this._getFullUser(req, {includePrefs: true}); const domain = getOrgFromRequest(req); const org = domain ? (await this._withSupportUserAllowedToView( domain, req, (scope) => this._dbManager.getOrg(scope, domain) @@ -534,10 +534,10 @@ export class ApiServer { })); } - private async _getFullUser(req: Request): Promise { + private async _getFullUser(req: Request, options: {includePrefs?: boolean} = {}): Promise { const mreq = req as RequestWithLogin; const userId = getUserId(mreq); - const user = await this._dbManager.getUser(userId); + const user = await this._dbManager.getUser(userId, options); if (!user) { throw new ApiError("unable to find user", 400); } const fullUser = this._dbManager.makeFullUser(user); diff --git a/app/gen-server/entity/User.ts b/app/gen-server/entity/User.ts index 59a55d74..2079b867 100644 --- a/app/gen-server/entity/User.ts +++ b/app/gen-server/entity/User.ts @@ -7,6 +7,7 @@ import {BaseEntity, BeforeInsert, Column, Entity, JoinTable, ManyToMany, OneToMa import {Group} from "./Group"; import {Login} from "./Login"; import {Organization} from "./Organization"; +import {Pref} from './Pref'; @Entity({name: 'users'}) export class User extends BaseEntity { @@ -34,6 +35,9 @@ export class User extends BaseEntity { @OneToMany(type => Login, login => login.user) public logins: Login[]; + @OneToMany(type => Pref, pref => pref.user) + public prefs: Pref[]; + @ManyToMany(type => Group) @JoinTable({ name: 'group_users', diff --git a/app/gen-server/lib/HomeDBManager.ts b/app/gen-server/lib/HomeDBManager.ts index 53d090f2..964210a3 100644 --- a/app/gen-server/lib/HomeDBManager.ts +++ b/app/gen-server/lib/HomeDBManager.ts @@ -482,8 +482,14 @@ export class HomeDBManager extends EventEmitter { return await User.findOne({where: {ref}, relations: ["logins"]}) || undefined; } - public async getUser(userId: number): Promise { - return await User.findOne({where: {id: userId}, relations: ["logins"]}) || undefined; + public async getUser( + userId: number, + options: {includePrefs?: boolean} = {} + ): Promise { + const {includePrefs} = options; + const relations = ["logins"]; + if (includePrefs) { relations.push("prefs"); } + return await User.findOne({where: {id: userId}, relations}) || undefined; } public async getFullUser(userId: number): Promise { @@ -505,7 +511,8 @@ export class HomeDBManager extends EventEmitter { name: user.name, picture: user.picture, ref: user.ref, - locale: user.options?.locale + locale: user.options?.locale, + prefs: user.prefs?.find((p)=> p.orgId === null)?.prefs, }; if (this.getAnonymousUserId() === user.id) { result.anonymous = true;