Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions apps/web/app/(all)/profile/appearance/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 (
Expand Down
38 changes: 32 additions & 6 deletions apps/web/ce/components/preferences/theme-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -65,7 +86,12 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
description={t(props.option.description)}
control={
<div>
<ThemeSwitch value={currentTheme} onChange={handleThemeChange} />
<ThemeSwitch
value={currentTheme}
onChange={(themeOption) => {
void handleThemeChange(themeOption);
}}
/>
</div>
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
16 changes: 13 additions & 3 deletions apps/web/core/components/power-k/config/preferences-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,22 @@ 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({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: t("power_k.preferences_actions.toast.theme.error"),
});
return;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -53,13 +59,15 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
title: t("toast.success"),
message: t("power_k.preferences_actions.toast.timezone.success"),
});
return;
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: t("power_k.preferences_actions.toast.timezone.error"),
});
return;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -75,13 +83,15 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
title: t("toast.success"),
message: t("power_k.preferences_actions.toast.generic.success"),
});
return;
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: t("toast.error"),
message: t("power_k.preferences_actions.toast.generic.error"),
});
return;
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/propel/src/icons/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
15 changes: 15 additions & 0 deletions packages/propel/src/icons/actions/upgrade-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IconWrapper } from "../icon-wrapper";
import type { ISvgIcons } from "../type";

export function UpgradeIcon({ color = "currentColor", ...rest }: ISvgIcons) {
return (
<IconWrapper color={color} {...rest}>
<path
fillRule="evenodd"
clipRule="evenodd"
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"
fill={color}
/>
</IconWrapper>
);
}
1 change: 1 addition & 0 deletions packages/propel/src/icons/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const ActionsIconsMap = [
{ icon: <Icon name="action.search" />, title: "SearchIcon" },
{ icon: <Icon name="action.preferences" />, title: "PreferencesIcon" },
{ icon: <Icon name="action.copy-link" />, title: "CopyLinkIcon" },
{ icon: <Icon name="action.upgrade" />, title: "UpgradeIcon" },
];

export const ArrowsIconsMap = [
Expand Down
2 changes: 2 additions & 0 deletions packages/propel/src/icons/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FilterIcon,
PreferencesIcon,
SearchIcon,
UpgradeIcon,
} from "./actions";
import { AddIcon } from "./actions/add-icon";
import { CloseIcon } from "./actions/close-icon";
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions packages/utils/src/theme/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};
13 changes: 12 additions & 1 deletion packages/utils/src/theme/theme-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
});
}

/**
Expand Down Expand Up @@ -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`);
});
}
Loading