Skip to content

Commit 2bc7080

Browse files
[WEB-5772] chore: theme switcher and editor colors enhancements (#8436)
1 parent 6cd85a7 commit 2bc7080

File tree

10 files changed

+142
-16
lines changed

10 files changed

+142
-16
lines changed

apps/web/app/(all)/profile/appearance/page.tsx

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { I_THEME_OPTION } from "@plane/constants";
66
import { THEME_OPTIONS } from "@plane/constants";
77
import { useTranslation } from "@plane/i18n";
88
import { setPromiseToast } from "@plane/propel/toast";
9+
import { applyCustomTheme } from "@plane/utils";
910
// components
1011
import { LogoSpinner } from "@/components/common/logo-spinner";
1112
import { PageHead } from "@/components/core/page-title";
@@ -30,22 +31,47 @@ function ProfileAppearancePage() {
3031
}, [userProfile?.theme?.theme]);
3132

3233
const handleThemeChange = useCallback(
33-
(themeOption: I_THEME_OPTION) => {
34+
async (themeOption: I_THEME_OPTION) => {
3435
setTheme(themeOption.value);
36+
37+
// If switching to custom theme and user has saved custom colors, apply them immediately
38+
if (
39+
themeOption.value === "custom" &&
40+
userProfile?.theme?.primary &&
41+
userProfile?.theme?.background &&
42+
userProfile?.theme?.darkPalette !== undefined
43+
) {
44+
applyCustomTheme(
45+
userProfile.theme.primary,
46+
userProfile.theme.background,
47+
userProfile.theme.darkPalette ? "dark" : "light"
48+
);
49+
}
50+
3551
const updateCurrentUserThemePromise = updateUserTheme({ theme: themeOption.value });
3652
setPromiseToast(updateCurrentUserThemePromise, {
3753
loading: "Updating theme...",
3854
success: {
39-
title: "Success!",
40-
message: () => "Theme updated successfully.",
55+
title: "Theme updated",
56+
message: () => "Reloading to apply changes...",
4157
},
4258
error: {
4359
title: "Error!",
44-
message: () => "Failed to update the theme.",
60+
message: () => "Failed to update theme. Please try again.",
4561
},
4662
});
63+
// Wait for the promise to resolve, then reload after showing toast
64+
try {
65+
await updateCurrentUserThemePromise;
66+
setTimeout(() => {
67+
window.location.reload();
68+
}, 1500);
69+
} catch (error) {
70+
// Error toast already shown by setPromiseToast
71+
console.error("Error updating theme:", error);
72+
}
4773
},
48-
[updateUserTheme]
74+
[setTheme, updateUserTheme, userProfile]
4975
);
5076

5177
return (

apps/web/ce/components/preferences/theme-switcher.tsx

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { I_THEME_OPTION } from "@plane/constants";
66
import { THEME_OPTIONS } from "@plane/constants";
77
import { useTranslation } from "@plane/i18n";
88
import { setPromiseToast } from "@plane/propel/toast";
9+
import { applyCustomTheme } from "@plane/utils";
910
// components
1011
import { CustomThemeSelector } from "@/components/core/theme/custom-theme-selector";
1112
import { ThemeSwitch } from "@/components/core/theme/theme-switch";
@@ -34,26 +35,46 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
3435
}, [userProfile?.theme?.theme]);
3536

3637
const handleThemeChange = useCallback(
37-
(themeOption: I_THEME_OPTION) => {
38+
async (themeOption: I_THEME_OPTION) => {
3839
try {
3940
setTheme(themeOption.value);
41+
42+
// If switching to custom theme and user has saved custom colors, apply them immediately
43+
if (
44+
themeOption.value === "custom" &&
45+
userProfile?.theme?.primary &&
46+
userProfile?.theme?.background &&
47+
userProfile?.theme?.darkPalette !== undefined
48+
) {
49+
applyCustomTheme(
50+
userProfile.theme.primary,
51+
userProfile.theme.background,
52+
userProfile.theme.darkPalette ? "dark" : "light"
53+
);
54+
}
55+
4056
const updatePromise = updateUserTheme({ theme: themeOption.value });
4157
setPromiseToast(updatePromise, {
4258
loading: "Updating theme...",
4359
success: {
44-
title: "Success!",
45-
message: () => "Theme updated successfully!",
60+
title: "Theme updated",
61+
message: () => "Reloading to apply changes...",
4662
},
4763
error: {
4864
title: "Error!",
49-
message: () => "Failed to update the theme",
65+
message: () => "Failed to update theme. Please try again.",
5066
},
5167
});
68+
// Wait for the promise to resolve, then reload after showing toast
69+
await updatePromise;
70+
setTimeout(() => {
71+
window.location.reload();
72+
}, 1500);
5273
} catch (error) {
5374
console.error("Error updating theme:", error);
5475
}
5576
},
56-
[updateUserTheme]
77+
[setTheme, updateUserTheme, userProfile]
5778
);
5879

5980
if (!userProfile) return null;
@@ -65,7 +86,12 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
6586
description={t(props.option.description)}
6687
control={
6788
<div>
68-
<ThemeSwitch value={currentTheme} onChange={handleThemeChange} />
89+
<ThemeSwitch
90+
value={currentTheme}
91+
onChange={(themeOption) => {
92+
void handleThemeChange(themeOption);
93+
}}
94+
/>
6995
</div>
7096
}
7197
/>

apps/web/core/components/core/theme/custom-theme-selector.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,12 @@ export const CustomThemeSelector = observer(function CustomThemeSelector() {
7272
setToast({
7373
type: TOAST_TYPE.SUCCESS,
7474
title: t("success"),
75-
message: t("theme_updated_successfully"),
75+
message: "Reloading to apply changes...",
7676
});
77+
// reload the page after showing the toast
78+
setTimeout(() => {
79+
window.location.reload();
80+
}, 1500);
7781
} catch (error) {
7882
console.error("Failed to apply theme:", error);
7983
setToast({

apps/web/core/components/power-k/config/preferences-commands.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,22 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
2828
.then(() => {
2929
setToast({
3030
type: TOAST_TYPE.SUCCESS,
31-
title: t("toast.success"),
32-
message: t("power_k.preferences_actions.toast.theme.success"),
31+
title: "Theme updated",
32+
message: "Reloading to apply changes...",
3333
});
34+
// reload the page after showing the toast
35+
setTimeout(() => {
36+
window.location.reload();
37+
}, 1500);
38+
return;
3439
})
3540
.catch(() => {
3641
setToast({
3742
type: TOAST_TYPE.ERROR,
3843
title: t("toast.error"),
3944
message: t("power_k.preferences_actions.toast.theme.error"),
4045
});
46+
return;
4147
});
4248
},
4349
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -53,13 +59,15 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
5359
title: t("toast.success"),
5460
message: t("power_k.preferences_actions.toast.timezone.success"),
5561
});
62+
return;
5663
})
5764
.catch(() => {
5865
setToast({
5966
type: TOAST_TYPE.ERROR,
6067
title: t("toast.error"),
6168
message: t("power_k.preferences_actions.toast.timezone.error"),
6269
});
70+
return;
6371
});
6472
},
6573
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -75,13 +83,15 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
7583
title: t("toast.success"),
7684
message: t("power_k.preferences_actions.toast.generic.success"),
7785
});
86+
return;
7887
})
7988
.catch(() => {
8089
setToast({
8190
type: TOAST_TYPE.ERROR,
8291
title: t("toast.error"),
8392
message: t("power_k.preferences_actions.toast.generic.error"),
8493
});
94+
return;
8595
});
8696
},
8797
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -98,7 +108,7 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
98108
icon: Palette,
99109
onSelect: (data) => {
100110
const theme = data as string;
101-
handleUpdateTheme(theme);
111+
void handleUpdateTheme(theme);
102112
},
103113
isEnabled: () => true,
104114
isVisible: () => true,

packages/propel/src/icons/actions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from "./filter-applied-icon";
88
export * from "./search-icon";
99
export * from "./preferences-icon";
1010
export * from "./copy-link";
11+
export * from "./upgrade-icon";
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { IconWrapper } from "../icon-wrapper";
2+
import type { ISvgIcons } from "../type";
3+
4+
export function UpgradeIcon({ color = "currentColor", ...rest }: ISvgIcons) {
5+
return (
6+
<IconWrapper color={color} {...rest}>
7+
<path
8+
fillRule="evenodd"
9+
clipRule="evenodd"
10+
d="M8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1ZM5.00457 7.55003L7.55003 5.00457C7.79853 4.75605 8.20147 4.75605 8.44997 5.00457L10.9954 7.55003C11.2439 7.79853 11.2439 8.20147 10.9954 8.44997C10.7469 8.69847 10.344 8.69847 10.0955 8.44997L8.63636 6.99085V10.5455C8.63636 10.8969 8.35146 11.1818 8 11.1818C7.64854 11.1818 7.36364 10.8969 7.36364 10.5455V6.99085L5.90452 8.44997C5.65601 8.69847 5.25309 8.69847 5.00457 8.44997C4.75605 8.20147 4.75605 7.79853 5.00457 7.55003Z"
11+
fill={color}
12+
/>
13+
</IconWrapper>
14+
);
15+
}

packages/propel/src/icons/constants.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const ActionsIconsMap = [
99
{ icon: <Icon name="action.search" />, title: "SearchIcon" },
1010
{ icon: <Icon name="action.preferences" />, title: "PreferencesIcon" },
1111
{ icon: <Icon name="action.copy-link" />, title: "CopyLinkIcon" },
12+
{ icon: <Icon name="action.upgrade" />, title: "UpgradeIcon" },
1213
];
1314

1415
export const ArrowsIconsMap = [

packages/propel/src/icons/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
FilterIcon,
66
PreferencesIcon,
77
SearchIcon,
8+
UpgradeIcon,
89
} from "./actions";
910
import { AddIcon } from "./actions/add-icon";
1011
import { CloseIcon } from "./actions/close-icon";
@@ -134,6 +135,7 @@ export const ICON_REGISTRY = {
134135
"action.search": SearchIcon,
135136
"action.preferences": PreferencesIcon,
136137
"action.copy-link": CopyLinkIcon,
138+
"action.upgrade": UpgradeIcon,
137139

138140
// Arrow icons
139141
"arrow.chevron-down": ChevronDownIcon,

packages/utils/src/theme/constants.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,33 @@ export type SaturationCurve = "ease-in-out" | "linear";
112112
* Default saturation curve
113113
*/
114114
export const DEFAULT_SATURATION_CURVE: SaturationCurve = "ease-in-out";
115+
116+
/**
117+
* Editor color backgrounds for light mode
118+
* Used for stickies and editor elements
119+
*/
120+
export const EDITOR_COLORS_LIGHT = {
121+
gray: "#d6d6d8",
122+
peach: "#ffd5d7",
123+
pink: "#fdd4e3",
124+
orange: "#ffe3cd",
125+
green: "#c3f0de",
126+
"light-blue": "#c5eff9",
127+
"dark-blue": "#c9dafb",
128+
purple: "#e3d8fd",
129+
};
130+
131+
/**
132+
* Editor color backgrounds for dark mode
133+
* Used for stickies and editor elements
134+
*/
135+
export const EDITOR_COLORS_DARK = {
136+
gray: "#404144",
137+
peach: "#593032",
138+
pink: "#562e3d",
139+
orange: "#583e2a",
140+
green: "#1d4a3b",
141+
"light-blue": "#1f495c",
142+
"dark-blue": "#223558",
143+
purple: "#3d325a",
144+
};

packages/utils/src/theme/theme-application.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { hexToOKLCH, oklchToCSS, getRelativeLuminance, getPerceptualBrightness } from "./color-conversion";
77
import type { OKLCH } from "./color-conversion";
8-
import { ALPHA_MAPPING } from "./constants";
8+
import { ALPHA_MAPPING, EDITOR_COLORS_LIGHT, EDITOR_COLORS_DARK } from "./constants";
99
import { generateThemePalettes } from "./palette-generator";
1010
import { getBrandMapping, getNeutralMapping, invertPalette } from "./theme-inversion";
1111

@@ -129,6 +129,12 @@ export function applyCustomTheme(brandColor: string, neutralColor: string, mode:
129129
const { textColor, iconColor } = getOnColorTextColors(brandColor, "wcag");
130130
themeElement.style.setProperty(`--text-color-on-color`, oklchToCSS(textColor));
131131
themeElement.style.setProperty(`--text-color-icon-on-color`, oklchToCSS(iconColor));
132+
133+
// Apply editor color backgrounds based on mode
134+
const editorColors = mode === "dark" ? EDITOR_COLORS_DARK : EDITOR_COLORS_LIGHT;
135+
Object.entries(editorColors).forEach(([color, value]) => {
136+
themeElement.style.setProperty(`--editor-colors-${color}-background`, value);
137+
});
132138
}
133139

134140
/**
@@ -173,4 +179,9 @@ export function clearCustomTheme(): void {
173179

174180
themeElement.style.removeProperty(`--text-color-on-color`);
175181
themeElement.style.removeProperty(`--text-color-icon-on-color`);
182+
183+
// Clear editor color background overrides
184+
Object.keys(EDITOR_COLORS_LIGHT).forEach((color) => {
185+
themeElement.style.removeProperty(`--editor-colors-${color}-background`);
186+
});
176187
}

0 commit comments

Comments
 (0)