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