1 import { c } from 'ttag';
4 import carbonTheme from '@proton/colors/themes/dist/carbon.theme.css';
6 import classicTheme from '@proton/colors/themes/dist/classic.theme.css';
8 import contrastDarkTheme from '@proton/colors/themes/dist/contrast-dark.theme.css';
10 import contrastLightTheme from '@proton/colors/themes/dist/contrast-light.theme.css';
12 import duotoneTheme from '@proton/colors/themes/dist/duotone.theme.css';
14 import legacyTheme from '@proton/colors/themes/dist/legacy.theme.css';
16 import monokaiTheme from '@proton/colors/themes/dist/monokai.theme.css';
18 import passDarkTheme from '@proton/colors/themes/dist/pass-dark.theme.css';
20 import passLightTheme from '@proton/colors/themes/dist/pass-light.theme.css';
23 import snowTheme from '@proton/colors/themes/dist/snow.theme.css';
25 import storefrontWalletTheme from '@proton/colors/themes/dist/storefront-wallet.theme.css';
27 import storefrontTheme from '@proton/colors/themes/dist/storefront.theme.css';
29 import walletLightTheme from '@proton/colors/themes/dist/wallet.theme.css';
30 import { decodeBase64URL, encodeBase64URL } from '@proton/shared/lib/helpers/encoding';
32 import { canGetInboxDesktopInfo, getInboxDesktopInfo, hasInboxDesktopFeature } from '../desktop/ipcHelpers';
33 import { isElectronApp } from '../helpers/desktop';
35 // Update the allowed values in the settings heartbeat
36 export enum ThemeTypes {
48 StorefrontWallet = 11,
52 export const DESKTOP_THEME_TYPES = {
53 Carbon: ThemeTypes.Carbon,
54 Snow: ThemeTypes.Snow,
57 export const PROTON_DEFAULT_THEME = ThemeTypes.Duotone;
59 type ThemeDefinition = {
67 identifier: ThemeTypes;
69 * Defines the default theme color for the application. This sometimes affects
70 * how the OS displays the site
72 themeColorMeta: string;
74 * Colour definition for the SVG thumbnail only
83 * The theme's stylesheet
88 export const PROTON_THEMES_MAP: Record<ThemeTypes, ThemeDefinition> = {
89 [ThemeTypes.Duotone]: {
91 identifier: ThemeTypes.Duotone,
92 themeColorMeta: '#1b1340',
99 theme: duotoneTheme.toString(),
101 [ThemeTypes.Carbon]: {
103 identifier: ThemeTypes.Carbon,
104 themeColorMeta: '#16141c',
106 prominent: '#372E45',
111 theme: carbonTheme.toString(),
113 [ThemeTypes.Monokai]: {
115 identifier: ThemeTypes.Monokai,
116 themeColorMeta: '#16141c',
118 prominent: '#16141C',
123 theme: monokaiTheme.toString(),
127 identifier: ThemeTypes.Snow,
128 themeColorMeta: 'white',
130 prominent: '#FFFFFF',
135 theme: snowTheme.toString(),
137 [ThemeTypes.ContrastLight]: {
139 identifier: ThemeTypes.ContrastLight,
140 themeColorMeta: 'white',
142 prominent: '#FFFFFF',
147 theme: contrastLightTheme.toString(),
149 [ThemeTypes.ContrastDark]: {
151 identifier: ThemeTypes.ContrastDark,
152 themeColorMeta: 'black',
154 prominent: '#131313',
159 theme: contrastDarkTheme.toString(),
161 [ThemeTypes.Legacy]: {
163 identifier: ThemeTypes.Legacy,
164 themeColorMeta: '#505061',
166 prominent: '#535364',
171 theme: legacyTheme.toString(),
173 [ThemeTypes.Classic]: {
175 identifier: ThemeTypes.Classic,
176 themeColorMeta: '#1c223d',
178 prominent: '#282F54',
183 theme: classicTheme.toString(),
185 [ThemeTypes.PassDark]: {
187 identifier: ThemeTypes.PassDark,
188 themeColorMeta: '#191927',
190 prominent: '#16141C',
195 theme: passDarkTheme.toString(),
197 [ThemeTypes.Storefront]: {
199 identifier: ThemeTypes.Storefront,
200 themeColorMeta: '#1d1738',
202 prominent: '#16141C',
207 theme: storefrontTheme.toString(),
209 [ThemeTypes.WalletLight]: {
210 label: 'WalletLight',
211 identifier: ThemeTypes.WalletLight,
212 themeColorMeta: '#f3f5f6',
214 prominent: '#16141C',
219 theme: walletLightTheme.toString(),
221 [ThemeTypes.StorefrontWallet]: {
222 label: 'StorefrontWallet',
223 identifier: ThemeTypes.StorefrontWallet,
224 themeColorMeta: 'white',
226 prominent: '#FFFFFF',
231 theme: storefrontWalletTheme.toString(),
233 [ThemeTypes.PassLight]: {
235 identifier: ThemeTypes.PassLight,
236 themeColorMeta: '#F6F5F8',
238 prominent: '#302D45',
243 theme: passLightTheme.toString(),
247 export const getDarkThemes = () => [
250 ThemeTypes.ContrastDark,
254 export const getProminentHeaderThemes = () => [ThemeTypes.Classic, ThemeTypes.Legacy];
256 export const getThemes = () => {
257 // Currently we only support some themes in the desktop app.
259 return Object.values(DESKTOP_THEME_TYPES).map((id) => PROTON_THEMES_MAP[id]);
269 ThemeTypes.ContrastDark,
270 ThemeTypes.ContrastLight,
271 ].map((id) => PROTON_THEMES_MAP[id]);
274 export const getPassThemes = () => {
275 return [ThemeTypes.PassDark, ThemeTypes.PassLight].map((id) => PROTON_THEMES_MAP[id]);
278 export enum ThemeModeSetting {
284 export enum ColorScheme {
289 export enum MotionModeSetting {
294 export enum ThemeFontSizeSetting {
302 interface ThemeFontSizeSettingValue {
307 export const ThemeFontSizeSettingMap: { [key in ThemeFontSizeSetting]: ThemeFontSizeSettingValue } = {
308 [ThemeFontSizeSetting.X_SMALL]: {
309 label: () => c('Font size option').t`Very small`,
312 [ThemeFontSizeSetting.SMALL]: {
313 label: () => c('Font size option').t`Small`,
316 [ThemeFontSizeSetting.DEFAULT]: {
317 label: () => c('Font size option').t`Medium (recommended)`,
320 [ThemeFontSizeSetting.LARGE]: {
321 label: () => c('Font size option').t`Large`,
324 [ThemeFontSizeSetting.X_LARGE]: {
325 label: () => c('Font size option').t`Very large`,
329 export const getThemeFontSizeEntries = () => {
330 return Object.entries(ThemeFontSizeSettingMap)
331 .map(([key, value]): [ThemeFontSizeSetting, ThemeFontSizeSettingValue] => {
332 const themeFontSizeSettingKey: ThemeFontSizeSetting = Number(key);
333 return [themeFontSizeSettingKey, value];
335 .sort((a, b) => a[1].value - b[1].value);
338 export enum ThemeFontFaceSetting {
346 interface ThemeFontFaceSettingValue {
348 value: string | null;
351 export const ThemeFontFaceSettingMap: { [key in ThemeFontFaceSetting]: ThemeFontFaceSettingValue } = {
352 [ThemeFontFaceSetting.DEFAULT]: {
355 This is the text proposed in a dropdown menu in the Accessibility settings.
356 Here the user can choose the "Font family", and this string proposes the choice of
357 "Theme font", the font of the chosen theme.
359 return c('Font face option').t`Theme font`;
363 [ThemeFontFaceSetting.SYSTEM]: {
366 This is the text proposed in a dropdown menu in the Accessibility settings.
367 Here the user can choose the "Font family", and this string proposes the choice of
368 "System default", the default font of the user's operating system.
370 return c('Font face option').t`System default`;
372 value: 'system-ui, sans-serif',
374 [ThemeFontFaceSetting.ARIAL]: {
375 label: () => 'Arial',
376 value: 'Arial, Helvetica, sans-serif',
378 [ThemeFontFaceSetting.TIMES]: {
379 label: () => 'Times New Roman',
380 value: "'Times New Roman', Times, serif",
382 [ThemeFontFaceSetting.DYSLEXIC]: {
383 label: () => 'OpenDyslexic',
384 value: 'OpenDyslexic, cursive',
387 export const getThemeFontFaceEntries = () => {
388 return Object.entries(ThemeFontFaceSettingMap).map(
389 ([key, value]): [ThemeFontFaceSetting, ThemeFontFaceSettingValue] => {
390 const themeFontFaceSettingKey: ThemeFontFaceSetting = Number(key);
391 return [themeFontFaceSettingKey, value];
396 export enum ThemeFeatureSetting {
402 export interface ThemeSetting {
403 Mode: ThemeModeSetting;
404 LightTheme: ThemeTypes;
405 DarkTheme: ThemeTypes;
406 FontSize: ThemeFontSizeSetting;
407 FontFace: ThemeFontFaceSetting;
408 Features: ThemeFeatureSetting;
411 export interface ThemeInformation {
414 prominentHeader: boolean;
418 colorScheme: ColorScheme;
419 motionMode: MotionModeSetting;
426 export const electronAppTheme: ThemeSetting = {
427 Mode: ThemeModeSetting.Auto,
428 LightTheme: ThemeTypes.Snow,
429 DarkTheme: ThemeTypes.Carbon,
430 FontSize: ThemeFontSizeSetting.DEFAULT,
431 FontFace: ThemeFontFaceSetting.DEFAULT,
432 Features: ThemeFeatureSetting.DEFAULT,
435 export const getDefaultThemeSetting = (themeType?: ThemeTypes): ThemeSetting => {
437 Mode: ThemeModeSetting.Light,
438 LightTheme: themeType || PROTON_DEFAULT_THEME,
439 DarkTheme: ThemeTypes.Carbon,
440 FontSize: ThemeFontSizeSetting.DEFAULT,
441 FontFace: ThemeFontFaceSetting.DEFAULT,
442 Features: ThemeFeatureSetting.DEFAULT,
445 // Electron follow system settings and only Snow and Carbon theme
447 if (hasInboxDesktopFeature('ThemeSelection') && canGetInboxDesktopInfo) {
448 return { ...electronAppTheme, ...getInboxDesktopInfo('theme') };
450 return electronAppTheme;
457 const getValidatedThemeType = (themeType: number): ThemeTypes | undefined => {
458 if (themeType >= ThemeTypes.Duotone && themeType <= ThemeTypes.StorefrontWallet) {
463 const getParsedThemeType = (maybeThemeType: any): ThemeTypes | undefined => {
464 return getValidatedThemeType(Number(maybeThemeType));
467 const getValidatedThemeMode = (maybeThemeMode: number | undefined): ThemeModeSetting | undefined => {
469 maybeThemeMode !== undefined &&
470 maybeThemeMode >= ThemeModeSetting.Auto &&
471 maybeThemeMode <= ThemeModeSetting.Light
473 return maybeThemeMode;
477 const getValidatedFontSize = (maybeFontSize: number | undefined) => {
479 maybeFontSize !== undefined &&
480 maybeFontSize >= ThemeFontSizeSetting.DEFAULT &&
481 maybeFontSize <= ThemeFontSizeSetting.X_LARGE
483 return maybeFontSize;
487 const getValidatedFontFace = (maybeFontFace: number | undefined) => {
489 maybeFontFace !== undefined &&
490 maybeFontFace >= ThemeFontFaceSetting.DEFAULT &&
491 maybeFontFace <= ThemeFontFaceSetting.DYSLEXIC
493 return maybeFontFace;
497 const getValidatedFeatures = (maybeFeatures: number | undefined) => {
498 if (maybeFeatures !== undefined && maybeFeatures >= 0 && maybeFeatures <= 32) {
499 return maybeFeatures;
503 export const getParsedThemeSetting = (storedThemeSetting: string | undefined): ThemeSetting => {
504 // Electron follow system settings and only Snow and Carbon theme
506 return getDefaultThemeSetting();
509 // The theme cookie used to contain just the theme number type.
510 if (storedThemeSetting && storedThemeSetting?.length === 1) {
511 const maybeParsedThemeType = getParsedThemeType(storedThemeSetting);
512 if (maybeParsedThemeType !== undefined) {
513 return getDefaultThemeSetting(maybeParsedThemeType);
516 const defaultThemeSetting = getDefaultThemeSetting(PROTON_DEFAULT_THEME);
517 // Now it contains JSON
518 if (storedThemeSetting && storedThemeSetting?.length >= 10) {
520 const parsedTheme: any = JSON.parse(decodeBase64URL(storedThemeSetting));
522 Mode: getValidatedThemeMode(parsedTheme.Mode) ?? defaultThemeSetting.Mode,
523 LightTheme: getValidatedThemeType(parsedTheme.LightTheme) ?? defaultThemeSetting.LightTheme,
524 DarkTheme: getValidatedThemeType(parsedTheme.DarkTheme) ?? defaultThemeSetting.DarkTheme,
525 FontFace: getValidatedFontSize(parsedTheme.FontFace) ?? defaultThemeSetting.FontFace,
526 FontSize: getValidatedFontFace(parsedTheme.FontSize) ?? defaultThemeSetting.FontSize,
527 Features: getValidatedFeatures(parsedTheme.Features) ?? defaultThemeSetting.Features,
531 return defaultThemeSetting;
534 const getDiff = (a: ThemeSetting, b: ThemeSetting): Partial<ThemeSetting> => {
535 return Object.entries(a).reduce<Partial<ThemeSetting>>((acc, [_key, value]) => {
536 const key = _key as keyof ThemeSetting;
537 const otherValue = b[key] as any;
538 if (value !== otherValue) {
539 acc[key] = otherValue;
545 export const serializeThemeSetting = (themeSetting: ThemeSetting) => {
546 const diff = getDiff(getDefaultThemeSetting(), themeSetting);
547 const keys = Object.keys(diff) as (keyof ThemeSetting)[];
551 if (keys.length === 1 && keys[0] === 'LightTheme') {
552 return `${diff.LightTheme}`;
554 return encodeBase64URL(JSON.stringify(diff));
557 export const getThemeType = (theme: ThemeSetting, colorScheme: ColorScheme): ThemeTypes => {
558 let value: ThemeTypes;
560 switch (theme.Mode) {
561 case ThemeModeSetting.Auto:
562 value = colorScheme === ColorScheme.Dark ? theme.DarkTheme : theme.LightTheme;
564 case ThemeModeSetting.Dark:
565 value = theme.DarkTheme;
568 case ThemeModeSetting.Light:
569 value = theme.LightTheme;
573 return getValidatedThemeType(value) ?? PROTON_DEFAULT_THEME;
576 export const PROTON_DEFAULT_THEME_SETTINGS: ThemeSetting = {
577 LightTheme: PROTON_DEFAULT_THEME,
578 DarkTheme: PROTON_DEFAULT_THEME,
579 Mode: ThemeModeSetting.Light,
580 FontSize: ThemeFontSizeSetting.DEFAULT,
581 FontFace: ThemeFontFaceSetting.DEFAULT,
582 Features: ThemeFeatureSetting.DEFAULT,
585 export const PROTON_DEFAULT_THEME_INFORMATION: ThemeInformation = {
586 theme: PROTON_DEFAULT_THEME,
588 prominentHeader: false,
592 colorScheme: ColorScheme.Light,
593 motionMode: MotionModeSetting.No_preference,
600 export const isDesktopThemeType = (value: unknown): value is ThemeTypes => {
601 return Object.values(DESKTOP_THEME_TYPES).some((theme) => theme === value);