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