mirror of
https://github.com/makeplane/plane.git
synced 2025-12-25 08:09:33 +01:00
[WEB-5772] chore: theme switcher and editor colors enhancements (#8436)
This commit is contained in:
committed by
GitHub
parent
6cd85a7095
commit
2bc7080d24
@@ -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 (
|
||||
|
||||
@@ -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={
|
||||
<div>
|
||||
<ThemeSwitch value={currentTheme} onChange={handleThemeChange} />
|
||||
<ThemeSwitch
|
||||
value={currentTheme}
|
||||
onChange={(themeOption) => {
|
||||
void handleThemeChange(themeOption);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
packages/propel/src/icons/actions/upgrade-icon.tsx
Normal file
15
packages/propel/src/icons/actions/upgrade-icon.tsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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`);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user