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`); + }); }