Path: blob/master/src/packages/frontend/account/account-preferences-appearance.tsx
6194 views
/*1* This file is part of CoCalc: Copyright © 2025 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Button, Card, Slider } from "antd";6import { debounce } from "lodash";7import { ReactElement, useMemo } from "react";8import { FormattedMessage, defineMessages, useIntl } from "react-intl";910import { Panel, Switch } from "@cocalc/frontend/antd-bootstrap";11import { redux, useTypedRedux } from "@cocalc/frontend/app-framework";12import {13A,14HelpIcon,15Icon,16IconName,17LabeledRow,18} from "@cocalc/frontend/components";19import { labels } from "@cocalc/frontend/i18n";20import {21A11Y,22ACCESSIBILITY_ICON,23DARK_MODE_ICON,24} from "@cocalc/util/consts/ui";25import { DARK_MODE_DEFAULTS } from "@cocalc/util/db-schema/accounts";26import { COLORS } from "@cocalc/util/theme";27import {28DARK_MODE_KEYS,29DARK_MODE_MINS,30get_dark_mode_config,31} from "./dark-mode";32import { EditorSettingsColorScheme } from "./editor-settings/color-schemes";33import { I18NSelector, I18N_MESSAGE, I18N_TITLE } from "./i18n-selector";34import { OtherSettings } from "./other-settings";35import { TerminalSettings } from "./terminal-settings";3637// Icon constant for account preferences section38export const APPEARANCE_ICON_NAME: IconName = "eye";3940// See https://github.com/sagemathinc/cocalc/issues/562041// There are weird bugs with relying only on mathjax, whereas our42// implementation of katex with a fallback to mathjax works very well.43// This makes it so katex can't be disabled.44const ALLOW_DISABLE_KATEX = false;4546export function katexIsEnabled() {47if (!ALLOW_DISABLE_KATEX) {48return true;49}50return redux.getStore("account")?.getIn(["other_settings", "katex"]) ?? true;51}5253const DARK_MODE_LABELS = defineMessages({54brightness: {55id: "account.other-settings.theme.dark_mode.brightness",56defaultMessage: "Brightness",57},58contrast: {59id: "account.other-settings.theme.dark_mode.contrast",60defaultMessage: "Contrast",61},62sepia: {63id: "account.other-settings.theme.dark_mode.sepia",64defaultMessage: "Sepia",65},66});6768const ACCESSIBILITY_MESSAGES = defineMessages({69title: {70id: "account.appearance.accessibility.title",71defaultMessage: "Accessibility",72},73enabled: {74id: "account.appearance.accessibility.enabled",75defaultMessage:76"<strong>Enable Accessibility Mode:</strong> optimize the user interface for accessibility features",77},78});7980export function AccountPreferencesAppearance() {81const intl = useIntl();82const other_settings = useTypedRedux("account", "other_settings");83const editor_settings = useTypedRedux("account", "editor_settings");84const font_size = useTypedRedux("account", "font_size");85const stripe_customer = useTypedRedux("account", "stripe_customer");86const kucalc = useTypedRedux("customize", "kucalc");8788function on_change(name: string, value: any): void {89redux.getActions("account").set_other_settings(name, value);90}9192function on_change_editor_settings(name: string, value: any): void {93redux.getActions("account").set_editor_settings(name, value);94}9596// Debounced version for dark mode sliders to reduce CPU usage97const on_change_dark_mode = useMemo(98() =>99debounce((name: string, value: any) => on_change(name, value), 50, {100trailing: true,101leading: false,102}),103[],104);105106function render_katex() {107if (!ALLOW_DISABLE_KATEX) {108return null;109}110return (111<Switch112checked={!!other_settings.get("katex")}113onChange={(e) => on_change("katex", e.target.checked)}114>115<FormattedMessage116id="account.other-settings.katex"117defaultMessage={`<strong>KaTeX:</strong> attempt to render formulas118using {katex} (much faster, but missing context menu options)`}119values={{ katex: <A href={"https://katex.org/"}>KaTeX</A> }}120/>121</Switch>122);123}124125function getAccessibilitySettings(): { enabled: boolean } {126const settingsStr = other_settings.get(A11Y);127if (!settingsStr) {128return { enabled: false };129}130try {131return JSON.parse(settingsStr);132} catch {133return { enabled: false };134}135}136137function setAccessibilitySettings(settings: { enabled: boolean }): void {138on_change(A11Y, JSON.stringify(settings));139}140141function renderAccessibilityPanel(): ReactElement {142const settings = getAccessibilitySettings();143return (144<Panel145size="small"146header={147<>148<Icon unicode={ACCESSIBILITY_ICON} />{" "}149{intl.formatMessage(ACCESSIBILITY_MESSAGES.title)}150</>151}152>153<Switch154checked={settings.enabled}155onChange={(e) =>156setAccessibilitySettings({ ...settings, enabled: e.target.checked })157}158>159<FormattedMessage {...ACCESSIBILITY_MESSAGES.enabled} />160</Switch>161</Panel>162);163}164165function renderDarkModePanel(): ReactElement {166const checked = !!other_settings.get("dark_mode");167const config = get_dark_mode_config(other_settings.toJS());168return (169<Panel170size="small"171header={172<>173<Icon unicode={DARK_MODE_ICON} /> Dark Mode174</>175}176styles={{177header: {178color: COLORS.GRAY_LLL,179backgroundColor: COLORS.GRAY_DD,180},181body: {182color: COLORS.GRAY_LLL,183backgroundColor: COLORS.GRAY_D,184},185}}186>187<div>188<Switch189checked={checked}190onChange={(e) => on_change("dark_mode", e.target.checked)}191labelStyle={{ color: COLORS.GRAY_LLL }}192>193<FormattedMessage194id="account.other-settings.theme.dark_mode.compact"195defaultMessage={`Dark mode: reduce eye strain by showing a dark background (via {DR})`}196values={{197DR: (198<A199style={{ color: "#e96c4d", fontWeight: 700 }}200href="https://darkreader.org/"201>202DARK READER203</A>204),205}}206/>207</Switch>208{checked ? (209<Card210size="small"211title={212<>213<Icon unicode={DARK_MODE_ICON} />{" "}214{intl.formatMessage({215id: "account.other-settings.theme.dark_mode.configuration",216defaultMessage: "Dark Mode Configuration",217})}218</>219}220>221<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>222{DARK_MODE_KEYS.map((key) => (223<div224key={key}225style={{ display: "flex", gap: 10, alignItems: "center" }}226>227<div style={{ width: 100 }}>228{intl.formatMessage(DARK_MODE_LABELS[key])}229</div>230<Slider231min={DARK_MODE_MINS[key]}232max={100}233value={config[key]}234onChange={(x) =>235on_change_dark_mode(`dark_mode_${key}`, x)236}237marks={{238[DARK_MODE_DEFAULTS[key]]: String(239DARK_MODE_DEFAULTS[key],240),241}}242style={{ flex: 1, width: 0 }}243/>244<Button245size="small"246style={{ marginLeft: "20px" }}247onClick={() =>248on_change_dark_mode(249`dark_mode_${key}`,250DARK_MODE_DEFAULTS[key],251)252}253>254{intl.formatMessage(labels.reset)}255</Button>256</div>257))}258</div>259</Card>260) : undefined}261</div>262</Panel>263);264}265266function renderUserInterfacePanel(): ReactElement {267return (268<Panel269size="small"270header={271<>272<Icon name="desktop" />{" "}273<FormattedMessage274id="account.appearance.user_interface.title"275defaultMessage="User Interface"276/>277</>278}279>280<LabeledRow281label={282<>283<Icon name="translation-outlined" />{" "}284{intl.formatMessage(labels.language)}285</>286}287>288<div>289<I18NSelector />{" "}290<HelpIcon title={intl.formatMessage(I18N_TITLE)}>291{intl.formatMessage(I18N_MESSAGE)}292</HelpIcon>293</div>294</LabeledRow>295<Switch296checked={!!other_settings.get("hide_file_popovers")}297onChange={(e) => on_change("hide_file_popovers", e.target.checked)}298>299<FormattedMessage300id="account.other-settings.file_popovers"301defaultMessage={`<strong>Hide File Tab Popovers:</strong>302do not show the popovers over file tabs`}303/>304</Switch>305<Switch306checked={!!other_settings.get("hide_project_popovers")}307onChange={(e) => on_change("hide_project_popovers", e.target.checked)}308>309<FormattedMessage310id="account.other-settings.project_popovers"311defaultMessage={`<strong>Hide Project Tab Popovers:</strong>312do not show the popovers over the project tabs`}313/>314</Switch>315<Switch316checked={!!other_settings.get("hide_button_tooltips")}317onChange={(e) => on_change("hide_button_tooltips", e.target.checked)}318>319<FormattedMessage320id="account.other-settings.button_tooltips"321defaultMessage={`<strong>Hide Button Tooltips:</strong>322hides some button tooltips (this is only partial)`}323/>324</Switch>325<Switch326checked={!!other_settings.get("time_ago_absolute")}327onChange={(e) => on_change("time_ago_absolute", e.target.checked)}328>329<FormattedMessage330id="account.other-settings.time_ago_absolute"331defaultMessage={`<strong>Display Timestamps as absolute points in time</strong>332instead of relative to the current time`}333/>334</Switch>335<Switch336checked={!!other_settings.get("hide_navbar_balance")}337onChange={(e) => on_change("hide_navbar_balance", e.target.checked)}338>339<FormattedMessage340id="account.other-settings.hide_navbar_balance"341defaultMessage={`<strong>Hide Account Balance</strong> in navigation bar`}342/>343</Switch>344{render_katex()}345</Panel>346);347}348349return (350<>351{renderUserInterfacePanel()}352<OtherSettings353other_settings={other_settings}354is_stripe_customer={355!!stripe_customer?.getIn(["subscriptions", "total_count"])356}357kucalc={kucalc}358mode="appearance"359/>360{renderDarkModePanel()}361{renderAccessibilityPanel()}362<EditorSettingsColorScheme363size="small"364theme={editor_settings?.get("theme") ?? "default"}365on_change={(value) => on_change_editor_settings("theme", value)}366editor_settings={editor_settings}367font_size={font_size}368/>369<TerminalSettings />370</>371);372}373374375