Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / shared / lib / themes / themes.ts
blobcabb578cc37384e89f1d4ef70d7a2bc352f2e6dd
1 import { c } from 'ttag';
3 // @ts-ignore
4 import carbonTheme from '@proton/colors/themes/dist/carbon.theme.css';
5 // @ts-ignore
6 import classicTheme from '@proton/colors/themes/dist/classic.theme.css';
7 // @ts-ignore
8 import contrastDarkTheme from '@proton/colors/themes/dist/contrast-dark.theme.css';
9 // @ts-ignore
10 import contrastLightTheme from '@proton/colors/themes/dist/contrast-light.theme.css';
11 // @ts-ignore
12 import duotoneTheme from '@proton/colors/themes/dist/duotone.theme.css';
13 // @ts-ignore
14 import legacyTheme from '@proton/colors/themes/dist/legacy.theme.css';
15 // @ts-ignore
16 import monokaiTheme from '@proton/colors/themes/dist/monokai.theme.css';
17 // @ts-ignore
18 import passDarkTheme from '@proton/colors/themes/dist/pass-dark.theme.css';
19 // @ts-ignore
20 import passLightTheme from '@proton/colors/themes/dist/pass-light.theme.css';
21 // @ts-ignore
22 // @ts-ignore
23 import snowTheme from '@proton/colors/themes/dist/snow.theme.css';
24 // @ts-ignore
25 import storefrontWalletTheme from '@proton/colors/themes/dist/storefront-wallet.theme.css';
26 // @ts-ignore
27 import storefrontTheme from '@proton/colors/themes/dist/storefront.theme.css';
28 // @ts-ignore
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 {
37     Duotone = 0,
38     Carbon = 1,
39     Snow = 2,
40     Monokai = 3,
41     ContrastLight = 4,
42     Legacy = 5,
43     Classic = 6,
44     ContrastDark = 7,
45     PassDark = 8,
46     Storefront = 9,
47     WalletLight = 10,
48     StorefrontWallet = 11,
49     PassLight = 12,
52 export const DESKTOP_THEME_TYPES = {
53     Carbon: ThemeTypes.Carbon,
54     Snow: ThemeTypes.Snow,
55 } as const;
57 export const PROTON_DEFAULT_THEME = ThemeTypes.Duotone;
59 type ThemeDefinition = {
60     /**
61      * Theme name
62      */
63     label: string;
64     /**
65      * Theme identifier
66      */
67     identifier: ThemeTypes;
68     /**
69      * Defines the default theme color for the application. This sometimes affects
70      * how the OS displays the site
71      */
72     themeColorMeta: string;
73     /**
74      * Colour definition for the SVG thumbnail only
75      */
76     thumbColors: {
77         prominent: string;
78         standard: string;
79         primary: string;
80         weak: string;
81     };
82     /**
83      * The theme's stylesheet
84      */
85     theme: string;
88 export const PROTON_THEMES_MAP: Record<ThemeTypes, ThemeDefinition> = {
89     [ThemeTypes.Duotone]: {
90         label: 'Proton',
91         identifier: ThemeTypes.Duotone,
92         themeColorMeta: '#1b1340',
93         thumbColors: {
94             prominent: '#44348C',
95             standard: '#ffffff',
96             primary: '#936DFF',
97             weak: '#9186BE',
98         },
99         theme: duotoneTheme.toString(),
100     },
101     [ThemeTypes.Carbon]: {
102         label: 'Carbon',
103         identifier: ThemeTypes.Carbon,
104         themeColorMeta: '#16141c',
105         thumbColors: {
106             prominent: '#372E45',
107             standard: '#453C56',
108             primary: '#936DFF',
109             weak: '#7A6E80',
110         },
111         theme: carbonTheme.toString(),
112     },
113     [ThemeTypes.Monokai]: {
114         label: 'Monokai',
115         identifier: ThemeTypes.Monokai,
116         themeColorMeta: '#16141c',
117         thumbColors: {
118             prominent: '#16141C',
119             standard: '#2B293D',
120             primary: '#D3597B',
121             weak: '#706878',
122         },
123         theme: monokaiTheme.toString(),
124     },
125     [ThemeTypes.Snow]: {
126         label: 'Snow',
127         identifier: ThemeTypes.Snow,
128         themeColorMeta: 'white',
129         thumbColors: {
130             prominent: '#FFFFFF',
131             standard: '#FAF8F6',
132             primary: '#6D4AFF',
133             weak: '#C7C4C1',
134         },
135         theme: snowTheme.toString(),
136     },
137     [ThemeTypes.ContrastLight]: {
138         label: 'Ivory',
139         identifier: ThemeTypes.ContrastLight,
140         themeColorMeta: 'white',
141         thumbColors: {
142             prominent: '#FFFFFF',
143             standard: '#FAF8F6',
144             primary: '#4E33BF',
145             weak: '#333333',
146         },
147         theme: contrastLightTheme.toString(),
148     },
149     [ThemeTypes.ContrastDark]: {
150         label: 'Ebony',
151         identifier: ThemeTypes.ContrastDark,
152         themeColorMeta: 'black',
153         thumbColors: {
154             prominent: '#131313',
155             standard: '#000000',
156             primary: '#8C94FD',
157             weak: '#555555',
158         },
159         theme: contrastDarkTheme.toString(),
160     },
161     [ThemeTypes.Legacy]: {
162         label: 'Legacy',
163         identifier: ThemeTypes.Legacy,
164         themeColorMeta: '#505061',
165         thumbColors: {
166             prominent: '#535364',
167             standard: '#F5F5F5',
168             primary: '#9498CB',
169             weak: '#BABAC1',
170         },
171         theme: legacyTheme.toString(),
172     },
173     [ThemeTypes.Classic]: {
174         label: 'Classic',
175         identifier: ThemeTypes.Classic,
176         themeColorMeta: '#1c223d',
177         thumbColors: {
178             prominent: '#282F54',
179             standard: '#F5F4F2',
180             primary: '#6A7FE0',
181             weak: '#585E78',
182         },
183         theme: classicTheme.toString(),
184     },
185     [ThemeTypes.PassDark]: {
186         label: 'Pass Dark',
187         identifier: ThemeTypes.PassDark,
188         themeColorMeta: '#191927',
189         thumbColors: {
190             prominent: '#16141C',
191             standard: '#2A2833',
192             primary: '#6D4AFF',
193             weak: '#6c6b70',
194         },
195         theme: passDarkTheme.toString(),
196     },
197     [ThemeTypes.Storefront]: {
198         label: 'Storefront',
199         identifier: ThemeTypes.Storefront,
200         themeColorMeta: '#1d1738',
201         thumbColors: {
202             prominent: '#16141C',
203             standard: '#2A2833',
204             primary: '#6D4AFF',
205             weak: '#6c6b70',
206         },
207         theme: storefrontTheme.toString(),
208     },
209     [ThemeTypes.WalletLight]: {
210         label: 'WalletLight',
211         identifier: ThemeTypes.WalletLight,
212         themeColorMeta: '#f3f5f6',
213         thumbColors: {
214             prominent: '#16141C',
215             standard: '#2A2833',
216             primary: '#6D4AFF',
217             weak: '#6c6b70',
218         },
219         theme: walletLightTheme.toString(),
220     },
221     [ThemeTypes.StorefrontWallet]: {
222         label: 'StorefrontWallet',
223         identifier: ThemeTypes.StorefrontWallet,
224         themeColorMeta: 'white',
225         thumbColors: {
226             prominent: '#FFFFFF',
227             standard: '#FAF8F6',
228             primary: '#767DFF',
229             weak: '#F3F5F6',
230         },
231         theme: storefrontWalletTheme.toString(),
232     },
233     [ThemeTypes.PassLight]: {
234         label: 'Pass Light',
235         identifier: ThemeTypes.PassLight,
236         themeColorMeta: '#F6F5F8',
237         thumbColors: {
238             prominent: '#302D45',
239             standard: '#F6F5F8',
240             primary: '#8A6EFF',
241             weak: '#F2EFFF',
242         },
243         theme: passLightTheme.toString(),
244     },
245 } as const;
247 export const getDarkThemes = () => [
248     ThemeTypes.Carbon,
249     ThemeTypes.Monokai,
250     ThemeTypes.ContrastDark,
251     ThemeTypes.PassDark,
254 export const getProminentHeaderThemes = () => [ThemeTypes.Classic, ThemeTypes.Legacy];
256 export const getThemes = () => {
257     // Currently we only support some themes in the desktop app.
258     if (isElectronApp) {
259         return Object.values(DESKTOP_THEME_TYPES).map((id) => PROTON_THEMES_MAP[id]);
260     }
262     return [
263         ThemeTypes.Duotone,
264         ThemeTypes.Classic,
265         ThemeTypes.Snow,
266         ThemeTypes.Legacy,
267         ThemeTypes.Carbon,
268         ThemeTypes.Monokai,
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 {
279     Auto,
280     Dark,
281     Light,
284 export enum ColorScheme {
285     Dark,
286     Light,
289 export enum MotionModeSetting {
290     No_preference,
291     Reduce,
294 export enum ThemeFontSizeSetting {
295     DEFAULT = 0,
296     X_SMALL,
297     SMALL,
298     LARGE,
299     X_LARGE,
302 interface ThemeFontSizeSettingValue {
303     label: () => string;
304     value: number;
307 export const ThemeFontSizeSettingMap: { [key in ThemeFontSizeSetting]: ThemeFontSizeSettingValue } = {
308     [ThemeFontSizeSetting.X_SMALL]: {
309         label: () => c('Font size option').t`Very small`,
310         value: 10,
311     },
312     [ThemeFontSizeSetting.SMALL]: {
313         label: () => c('Font size option').t`Small`,
314         value: 12,
315     },
316     [ThemeFontSizeSetting.DEFAULT]: {
317         label: () => c('Font size option').t`Medium (recommended)`,
318         value: 14,
319     },
320     [ThemeFontSizeSetting.LARGE]: {
321         label: () => c('Font size option').t`Large`,
322         value: 16,
323     },
324     [ThemeFontSizeSetting.X_LARGE]: {
325         label: () => c('Font size option').t`Very large`,
326         value: 18,
327     },
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];
334         })
335         .sort((a, b) => a[1].value - b[1].value);
338 export enum ThemeFontFaceSetting {
339     DEFAULT,
340     SYSTEM,
341     ARIAL,
342     TIMES,
343     DYSLEXIC,
346 interface ThemeFontFaceSettingValue {
347     label: () => string;
348     value: string | null;
351 export const ThemeFontFaceSettingMap: { [key in ThemeFontFaceSetting]: ThemeFontFaceSettingValue } = {
352     [ThemeFontFaceSetting.DEFAULT]: {
353         label: () => {
354             /* translator:
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.
358             */
359             return c('Font face option').t`Theme font`;
360         },
361         value: null,
362     },
363     [ThemeFontFaceSetting.SYSTEM]: {
364         label: () => {
365             /* translator:
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.
369             */
370             return c('Font face option').t`System default`;
371         },
372         value: 'system-ui, sans-serif',
373     },
374     [ThemeFontFaceSetting.ARIAL]: {
375         label: () => 'Arial',
376         value: 'Arial, Helvetica, sans-serif',
377     },
378     [ThemeFontFaceSetting.TIMES]: {
379         label: () => 'Times New Roman',
380         value: "'Times New Roman', Times, serif",
381     },
382     [ThemeFontFaceSetting.DYSLEXIC]: {
383         label: () => 'OpenDyslexic',
384         value: 'OpenDyslexic, cursive',
385     },
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];
392         }
393     );
396 export enum ThemeFeatureSetting {
397     DEFAULT,
398     SCROLLBARS_OFF,
399     ANIMATIONS_OFF,
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 {
412     theme: ThemeTypes;
413     dark: boolean;
414     prominentHeader: boolean;
415     default: boolean;
416     style: string;
417     label: string;
418     colorScheme: ColorScheme;
419     motionMode: MotionModeSetting;
420     features: {
421         scrollbars: boolean;
422         animations: boolean;
423     };
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 => {
436     const theme = {
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,
443     };
445     // Electron follow system settings and only Snow and Carbon theme
446     if (isElectronApp) {
447         if (hasInboxDesktopFeature('ThemeSelection') && canGetInboxDesktopInfo) {
448             return { ...electronAppTheme, ...getInboxDesktopInfo('theme') };
449         } else {
450             return electronAppTheme;
451         }
452     }
454     return theme;
457 const getValidatedThemeType = (themeType: number): ThemeTypes | undefined => {
458     if (themeType >= ThemeTypes.Duotone && themeType <= ThemeTypes.StorefrontWallet) {
459         return themeType;
460     }
463 const getParsedThemeType = (maybeThemeType: any): ThemeTypes | undefined => {
464     return getValidatedThemeType(Number(maybeThemeType));
467 const getValidatedThemeMode = (maybeThemeMode: number | undefined): ThemeModeSetting | undefined => {
468     if (
469         maybeThemeMode !== undefined &&
470         maybeThemeMode >= ThemeModeSetting.Auto &&
471         maybeThemeMode <= ThemeModeSetting.Light
472     ) {
473         return maybeThemeMode;
474     }
477 const getValidatedFontSize = (maybeFontSize: number | undefined) => {
478     if (
479         maybeFontSize !== undefined &&
480         maybeFontSize >= ThemeFontSizeSetting.DEFAULT &&
481         maybeFontSize <= ThemeFontSizeSetting.X_LARGE
482     ) {
483         return maybeFontSize;
484     }
487 const getValidatedFontFace = (maybeFontFace: number | undefined) => {
488     if (
489         maybeFontFace !== undefined &&
490         maybeFontFace >= ThemeFontFaceSetting.DEFAULT &&
491         maybeFontFace <= ThemeFontFaceSetting.DYSLEXIC
492     ) {
493         return maybeFontFace;
494     }
497 const getValidatedFeatures = (maybeFeatures: number | undefined) => {
498     if (maybeFeatures !== undefined && maybeFeatures >= 0 && maybeFeatures <= 32) {
499         return maybeFeatures;
500     }
503 export const getParsedThemeSetting = (storedThemeSetting: string | undefined): ThemeSetting => {
504     // Electron follow system settings and only Snow and Carbon theme
505     if (isElectronApp) {
506         return getDefaultThemeSetting();
507     }
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);
514         }
515     }
516     const defaultThemeSetting = getDefaultThemeSetting(PROTON_DEFAULT_THEME);
517     // Now it contains JSON
518     if (storedThemeSetting && storedThemeSetting?.length >= 10) {
519         try {
520             const parsedTheme: any = JSON.parse(decodeBase64URL(storedThemeSetting));
521             return {
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,
528             };
529         } catch (e: any) {}
530     }
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;
540         }
541         return acc;
542     }, {});
545 export const serializeThemeSetting = (themeSetting: ThemeSetting) => {
546     const diff = getDiff(getDefaultThemeSetting(), themeSetting);
547     const keys = Object.keys(diff) as (keyof ThemeSetting)[];
548     if (!keys.length) {
549         return;
550     }
551     if (keys.length === 1 && keys[0] === 'LightTheme') {
552         return `${diff.LightTheme}`;
553     }
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;
563             break;
564         case ThemeModeSetting.Dark:
565             value = theme.DarkTheme;
566             break;
567         default:
568         case ThemeModeSetting.Light:
569             value = theme.LightTheme;
570             break;
571     }
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,
587     dark: false,
588     prominentHeader: false,
589     default: false,
590     style: '',
591     label: '',
592     colorScheme: ColorScheme.Light,
593     motionMode: MotionModeSetting.No_preference,
594     features: {
595         scrollbars: false,
596         animations: false,
597     },
600 export const isDesktopThemeType = (value: unknown): value is ThemeTypes => {
601     return Object.values(DESKTOP_THEME_TYPES).some((theme) => theme === value);