diff --git a/apps/web/app/(all)/profile/appearance/page.tsx b/apps/web/app/(all)/profile/appearance/page.tsx
index 33102b062b..0104310217 100644
--- a/apps/web/app/(all)/profile/appearance/page.tsx
+++ b/apps/web/app/(all)/profile/appearance/page.tsx
@@ -6,6 +6,7 @@ import type { I_THEME_OPTION } from "@plane/constants";
import { THEME_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { setPromiseToast } from "@plane/propel/toast";
+import { applyCustomTheme } from "@plane/utils";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { PageHead } from "@/components/core/page-title";
@@ -30,22 +31,47 @@ function ProfileAppearancePage() {
}, [userProfile?.theme?.theme]);
const handleThemeChange = useCallback(
- (themeOption: I_THEME_OPTION) => {
+ async (themeOption: I_THEME_OPTION) => {
setTheme(themeOption.value);
+
+ // If switching to custom theme and user has saved custom colors, apply them immediately
+ if (
+ themeOption.value === "custom" &&
+ userProfile?.theme?.primary &&
+ userProfile?.theme?.background &&
+ userProfile?.theme?.darkPalette !== undefined
+ ) {
+ applyCustomTheme(
+ userProfile.theme.primary,
+ userProfile.theme.background,
+ userProfile.theme.darkPalette ? "dark" : "light"
+ );
+ }
+
const updateCurrentUserThemePromise = updateUserTheme({ theme: themeOption.value });
setPromiseToast(updateCurrentUserThemePromise, {
loading: "Updating theme...",
success: {
- title: "Success!",
- message: () => "Theme updated successfully.",
+ title: "Theme updated",
+ message: () => "Reloading to apply changes...",
},
error: {
title: "Error!",
- message: () => "Failed to update the theme.",
+ message: () => "Failed to update theme. Please try again.",
},
});
+ // Wait for the promise to resolve, then reload after showing toast
+ try {
+ await updateCurrentUserThemePromise;
+ setTimeout(() => {
+ window.location.reload();
+ }, 1500);
+ } catch (error) {
+ // Error toast already shown by setPromiseToast
+ console.error("Error updating theme:", error);
+ }
},
- [updateUserTheme]
+ [setTheme, updateUserTheme, userProfile]
);
return (
diff --git a/apps/web/ce/components/preferences/theme-switcher.tsx b/apps/web/ce/components/preferences/theme-switcher.tsx
index cdb8ecb40f..9de9b96162 100644
--- a/apps/web/ce/components/preferences/theme-switcher.tsx
+++ b/apps/web/ce/components/preferences/theme-switcher.tsx
@@ -6,6 +6,7 @@ import type { I_THEME_OPTION } from "@plane/constants";
import { THEME_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { setPromiseToast } from "@plane/propel/toast";
+import { applyCustomTheme } from "@plane/utils";
// components
import { CustomThemeSelector } from "@/components/core/theme/custom-theme-selector";
import { ThemeSwitch } from "@/components/core/theme/theme-switch";
@@ -34,26 +35,46 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
}, [userProfile?.theme?.theme]);
const handleThemeChange = useCallback(
- (themeOption: I_THEME_OPTION) => {
+ async (themeOption: I_THEME_OPTION) => {
try {
setTheme(themeOption.value);
+
+ // If switching to custom theme and user has saved custom colors, apply them immediately
+ if (
+ themeOption.value === "custom" &&
+ userProfile?.theme?.primary &&
+ userProfile?.theme?.background &&
+ userProfile?.theme?.darkPalette !== undefined
+ ) {
+ applyCustomTheme(
+ userProfile.theme.primary,
+ userProfile.theme.background,
+ userProfile.theme.darkPalette ? "dark" : "light"
+ );
+ }
+
const updatePromise = updateUserTheme({ theme: themeOption.value });
setPromiseToast(updatePromise, {
loading: "Updating theme...",
success: {
- title: "Success!",
- message: () => "Theme updated successfully!",
+ title: "Theme updated",
+ message: () => "Reloading to apply changes...",
},
error: {
title: "Error!",
- message: () => "Failed to update the theme",
+ message: () => "Failed to update theme. Please try again.",
},
});
+ // Wait for the promise to resolve, then reload after showing toast
+ await updatePromise;
+ setTimeout(() => {
+ window.location.reload();
+ }, 1500);
} catch (error) {
console.error("Error updating theme:", error);
}
},
- [updateUserTheme]
+ [setTheme, updateUserTheme, userProfile]
);
if (!userProfile) return null;
@@ -65,7 +86,12 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
description={t(props.option.description)}
control={
-
+ {
+ void handleThemeChange(themeOption);
+ }}
+ />
}
/>
diff --git a/apps/web/core/components/core/theme/custom-theme-selector.tsx b/apps/web/core/components/core/theme/custom-theme-selector.tsx
index a249ec20e0..1091966e1c 100644
--- a/apps/web/core/components/core/theme/custom-theme-selector.tsx
+++ b/apps/web/core/components/core/theme/custom-theme-selector.tsx
@@ -72,8 +72,12 @@ export const CustomThemeSelector = observer(function CustomThemeSelector() {
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("success"),
- message: t("theme_updated_successfully"),
+ message: "Reloading to apply changes...",
});
+ // reload the page after showing the toast
+ setTimeout(() => {
+ window.location.reload();
+ }, 1500);
} catch (error) {
console.error("Failed to apply theme:", error);
setToast({
diff --git a/apps/web/core/components/power-k/config/preferences-commands.ts b/apps/web/core/components/power-k/config/preferences-commands.ts
index 5f86886ac9..116b0ddba9 100644
--- a/apps/web/core/components/power-k/config/preferences-commands.ts
+++ b/apps/web/core/components/power-k/config/preferences-commands.ts
@@ -28,9 +28,14 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
- title: t("toast.success"),
- message: t("power_k.preferences_actions.toast.theme.success"),
+ title: "Theme updated",
+ message: "Reloading to apply changes...",
});
+ // reload the page after showing the toast
+ setTimeout(() => {
+ window.location.reload();
+ }, 1500);
+ return;
})
.catch(() => {
setToast({
@@ -38,6 +43,7 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
title: t("toast.error"),
message: t("power_k.preferences_actions.toast.theme.error"),
});
+ return;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -53,6 +59,7 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
title: t("toast.success"),
message: t("power_k.preferences_actions.toast.timezone.success"),
});
+ return;
})
.catch(() => {
setToast({
@@ -60,6 +67,7 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
title: t("toast.error"),
message: t("power_k.preferences_actions.toast.timezone.error"),
});
+ return;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -75,6 +83,7 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
title: t("toast.success"),
message: t("power_k.preferences_actions.toast.generic.success"),
});
+ return;
})
.catch(() => {
setToast({
@@ -82,6 +91,7 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
title: t("toast.error"),
message: t("power_k.preferences_actions.toast.generic.error"),
});
+ return;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -98,7 +108,7 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
icon: Palette,
onSelect: (data) => {
const theme = data as string;
- handleUpdateTheme(theme);
+ void handleUpdateTheme(theme);
},
isEnabled: () => true,
isVisible: () => true,
diff --git a/packages/propel/src/icons/actions/index.ts b/packages/propel/src/icons/actions/index.ts
index a5249e90be..4409c29b21 100644
--- a/packages/propel/src/icons/actions/index.ts
+++ b/packages/propel/src/icons/actions/index.ts
@@ -8,3 +8,4 @@ export * from "./filter-applied-icon";
export * from "./search-icon";
export * from "./preferences-icon";
export * from "./copy-link";
+export * from "./upgrade-icon";
diff --git a/packages/propel/src/icons/actions/upgrade-icon.tsx b/packages/propel/src/icons/actions/upgrade-icon.tsx
new file mode 100644
index 0000000000..6c9ba9a29d
--- /dev/null
+++ b/packages/propel/src/icons/actions/upgrade-icon.tsx
@@ -0,0 +1,15 @@
+import { IconWrapper } from "../icon-wrapper";
+import type { ISvgIcons } from "../type";
+
+export function UpgradeIcon({ color = "currentColor", ...rest }: ISvgIcons) {
+ return (
+
+
+
+ );
+}
diff --git a/packages/propel/src/icons/constants.tsx b/packages/propel/src/icons/constants.tsx
index e57721f199..de3c08c1fb 100644
--- a/packages/propel/src/icons/constants.tsx
+++ b/packages/propel/src/icons/constants.tsx
@@ -9,6 +9,7 @@ export const ActionsIconsMap = [
{ icon: , title: "SearchIcon" },
{ icon: , title: "PreferencesIcon" },
{ icon: , title: "CopyLinkIcon" },
+ { icon: , title: "UpgradeIcon" },
];
export const ArrowsIconsMap = [
diff --git a/packages/propel/src/icons/registry.ts b/packages/propel/src/icons/registry.ts
index 715f80a201..84b1605946 100644
--- a/packages/propel/src/icons/registry.ts
+++ b/packages/propel/src/icons/registry.ts
@@ -5,6 +5,7 @@ import {
FilterIcon,
PreferencesIcon,
SearchIcon,
+ UpgradeIcon,
} from "./actions";
import { AddIcon } from "./actions/add-icon";
import { CloseIcon } from "./actions/close-icon";
@@ -134,6 +135,7 @@ export const ICON_REGISTRY = {
"action.search": SearchIcon,
"action.preferences": PreferencesIcon,
"action.copy-link": CopyLinkIcon,
+ "action.upgrade": UpgradeIcon,
// Arrow icons
"arrow.chevron-down": ChevronDownIcon,
diff --git a/packages/utils/src/theme/constants.ts b/packages/utils/src/theme/constants.ts
index eca1f65105..a30b7683cb 100644
--- a/packages/utils/src/theme/constants.ts
+++ b/packages/utils/src/theme/constants.ts
@@ -112,3 +112,33 @@ export type SaturationCurve = "ease-in-out" | "linear";
* Default saturation curve
*/
export const DEFAULT_SATURATION_CURVE: SaturationCurve = "ease-in-out";
+
+/**
+ * Editor color backgrounds for light mode
+ * Used for stickies and editor elements
+ */
+export const EDITOR_COLORS_LIGHT = {
+ gray: "#d6d6d8",
+ peach: "#ffd5d7",
+ pink: "#fdd4e3",
+ orange: "#ffe3cd",
+ green: "#c3f0de",
+ "light-blue": "#c5eff9",
+ "dark-blue": "#c9dafb",
+ purple: "#e3d8fd",
+};
+
+/**
+ * Editor color backgrounds for dark mode
+ * Used for stickies and editor elements
+ */
+export const EDITOR_COLORS_DARK = {
+ gray: "#404144",
+ peach: "#593032",
+ pink: "#562e3d",
+ orange: "#583e2a",
+ green: "#1d4a3b",
+ "light-blue": "#1f495c",
+ "dark-blue": "#223558",
+ purple: "#3d325a",
+};
diff --git a/packages/utils/src/theme/theme-application.ts b/packages/utils/src/theme/theme-application.ts
index 66041007ab..5e8ce421fb 100644
--- a/packages/utils/src/theme/theme-application.ts
+++ b/packages/utils/src/theme/theme-application.ts
@@ -5,7 +5,7 @@
import { hexToOKLCH, oklchToCSS, getRelativeLuminance, getPerceptualBrightness } from "./color-conversion";
import type { OKLCH } from "./color-conversion";
-import { ALPHA_MAPPING } from "./constants";
+import { ALPHA_MAPPING, EDITOR_COLORS_LIGHT, EDITOR_COLORS_DARK } from "./constants";
import { generateThemePalettes } from "./palette-generator";
import { getBrandMapping, getNeutralMapping, invertPalette } from "./theme-inversion";
@@ -129,6 +129,12 @@ export function applyCustomTheme(brandColor: string, neutralColor: string, mode:
const { textColor, iconColor } = getOnColorTextColors(brandColor, "wcag");
themeElement.style.setProperty(`--text-color-on-color`, oklchToCSS(textColor));
themeElement.style.setProperty(`--text-color-icon-on-color`, oklchToCSS(iconColor));
+
+ // Apply editor color backgrounds based on mode
+ const editorColors = mode === "dark" ? EDITOR_COLORS_DARK : EDITOR_COLORS_LIGHT;
+ Object.entries(editorColors).forEach(([color, value]) => {
+ themeElement.style.setProperty(`--editor-colors-${color}-background`, value);
+ });
}
/**
@@ -173,4 +179,9 @@ export function clearCustomTheme(): void {
themeElement.style.removeProperty(`--text-color-on-color`);
themeElement.style.removeProperty(`--text-color-icon-on-color`);
+
+ // Clear editor color background overrides
+ Object.keys(EDITOR_COLORS_LIGHT).forEach((color) => {
+ themeElement.style.removeProperty(`--editor-colors-${color}-background`);
+ });
}