[WEB-5772] chore: theme switcher and editor colors enhancements (#8436)
This commit is contained in:
parent
6cd85a7095
commit
2bc7080d24
10 changed files with 142 additions and 16 deletions
|
|
@ -6,6 +6,7 @@ import type { I_THEME_OPTION } from "@plane/constants";
|
||||||
import { THEME_OPTIONS } from "@plane/constants";
|
import { THEME_OPTIONS } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { setPromiseToast } from "@plane/propel/toast";
|
import { setPromiseToast } from "@plane/propel/toast";
|
||||||
|
import { applyCustomTheme } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||||
import { PageHead } from "@/components/core/page-title";
|
import { PageHead } from "@/components/core/page-title";
|
||||||
|
|
@ -30,22 +31,47 @@ function ProfileAppearancePage() {
|
||||||
}, [userProfile?.theme?.theme]);
|
}, [userProfile?.theme?.theme]);
|
||||||
|
|
||||||
const handleThemeChange = useCallback(
|
const handleThemeChange = useCallback(
|
||||||
(themeOption: I_THEME_OPTION) => {
|
async (themeOption: I_THEME_OPTION) => {
|
||||||
setTheme(themeOption.value);
|
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 });
|
const updateCurrentUserThemePromise = updateUserTheme({ theme: themeOption.value });
|
||||||
setPromiseToast(updateCurrentUserThemePromise, {
|
setPromiseToast(updateCurrentUserThemePromise, {
|
||||||
loading: "Updating theme...",
|
loading: "Updating theme...",
|
||||||
success: {
|
success: {
|
||||||
title: "Success!",
|
title: "Theme updated",
|
||||||
message: () => "Theme updated successfully.",
|
message: () => "Reloading to apply changes...",
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
title: "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 (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type { I_THEME_OPTION } from "@plane/constants";
|
||||||
import { THEME_OPTIONS } from "@plane/constants";
|
import { THEME_OPTIONS } from "@plane/constants";
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { setPromiseToast } from "@plane/propel/toast";
|
import { setPromiseToast } from "@plane/propel/toast";
|
||||||
|
import { applyCustomTheme } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { CustomThemeSelector } from "@/components/core/theme/custom-theme-selector";
|
import { CustomThemeSelector } from "@/components/core/theme/custom-theme-selector";
|
||||||
import { ThemeSwitch } from "@/components/core/theme/theme-switch";
|
import { ThemeSwitch } from "@/components/core/theme/theme-switch";
|
||||||
|
|
@ -34,26 +35,46 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
|
||||||
}, [userProfile?.theme?.theme]);
|
}, [userProfile?.theme?.theme]);
|
||||||
|
|
||||||
const handleThemeChange = useCallback(
|
const handleThemeChange = useCallback(
|
||||||
(themeOption: I_THEME_OPTION) => {
|
async (themeOption: I_THEME_OPTION) => {
|
||||||
try {
|
try {
|
||||||
setTheme(themeOption.value);
|
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 });
|
const updatePromise = updateUserTheme({ theme: themeOption.value });
|
||||||
setPromiseToast(updatePromise, {
|
setPromiseToast(updatePromise, {
|
||||||
loading: "Updating theme...",
|
loading: "Updating theme...",
|
||||||
success: {
|
success: {
|
||||||
title: "Success!",
|
title: "Theme updated",
|
||||||
message: () => "Theme updated successfully!",
|
message: () => "Reloading to apply changes...",
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
title: "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) {
|
} catch (error) {
|
||||||
console.error("Error updating theme:", error);
|
console.error("Error updating theme:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateUserTheme]
|
[setTheme, updateUserTheme, userProfile]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!userProfile) return null;
|
if (!userProfile) return null;
|
||||||
|
|
@ -65,7 +86,12 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
|
||||||
description={t(props.option.description)}
|
description={t(props.option.description)}
|
||||||
control={
|
control={
|
||||||
<div>
|
<div>
|
||||||
<ThemeSwitch value={currentTheme} onChange={handleThemeChange} />
|
<ThemeSwitch
|
||||||
|
value={currentTheme}
|
||||||
|
onChange={(themeOption) => {
|
||||||
|
void handleThemeChange(themeOption);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,12 @@ export const CustomThemeSelector = observer(function CustomThemeSelector() {
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: t("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) {
|
} catch (error) {
|
||||||
console.error("Failed to apply theme:", error);
|
console.error("Failed to apply theme:", error);
|
||||||
setToast({
|
setToast({
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,14 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: t("toast.success"),
|
title: "Theme updated",
|
||||||
message: t("power_k.preferences_actions.toast.theme.success"),
|
message: "Reloading to apply changes...",
|
||||||
});
|
});
|
||||||
|
// reload the page after showing the toast
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1500);
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setToast({
|
setToast({
|
||||||
|
|
@ -38,6 +43,7 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
|
||||||
title: t("toast.error"),
|
title: t("toast.error"),
|
||||||
message: t("power_k.preferences_actions.toast.theme.error"),
|
message: t("power_k.preferences_actions.toast.theme.error"),
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|
@ -53,6 +59,7 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
|
||||||
title: t("toast.success"),
|
title: t("toast.success"),
|
||||||
message: t("power_k.preferences_actions.toast.timezone.success"),
|
message: t("power_k.preferences_actions.toast.timezone.success"),
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setToast({
|
setToast({
|
||||||
|
|
@ -60,6 +67,7 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
|
||||||
title: t("toast.error"),
|
title: t("toast.error"),
|
||||||
message: t("power_k.preferences_actions.toast.timezone.error"),
|
message: t("power_k.preferences_actions.toast.timezone.error"),
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|
@ -75,6 +83,7 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
|
||||||
title: t("toast.success"),
|
title: t("toast.success"),
|
||||||
message: t("power_k.preferences_actions.toast.generic.success"),
|
message: t("power_k.preferences_actions.toast.generic.success"),
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setToast({
|
setToast({
|
||||||
|
|
@ -82,6 +91,7 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
|
||||||
title: t("toast.error"),
|
title: t("toast.error"),
|
||||||
message: t("power_k.preferences_actions.toast.generic.error"),
|
message: t("power_k.preferences_actions.toast.generic.error"),
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|
@ -98,7 +108,7 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
|
||||||
icon: Palette,
|
icon: Palette,
|
||||||
onSelect: (data) => {
|
onSelect: (data) => {
|
||||||
const theme = data as string;
|
const theme = data as string;
|
||||||
handleUpdateTheme(theme);
|
void handleUpdateTheme(theme);
|
||||||
},
|
},
|
||||||
isEnabled: () => true,
|
isEnabled: () => true,
|
||||||
isVisible: () => true,
|
isVisible: () => true,
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,4 @@ export * from "./filter-applied-icon";
|
||||||
export * from "./search-icon";
|
export * from "./search-icon";
|
||||||
export * from "./preferences-icon";
|
export * from "./preferences-icon";
|
||||||
export * from "./copy-link";
|
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.search" />, title: "SearchIcon" },
|
||||||
{ icon: <Icon name="action.preferences" />, title: "PreferencesIcon" },
|
{ icon: <Icon name="action.preferences" />, title: "PreferencesIcon" },
|
||||||
{ icon: <Icon name="action.copy-link" />, title: "CopyLinkIcon" },
|
{ icon: <Icon name="action.copy-link" />, title: "CopyLinkIcon" },
|
||||||
|
{ icon: <Icon name="action.upgrade" />, title: "UpgradeIcon" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ArrowsIconsMap = [
|
export const ArrowsIconsMap = [
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
FilterIcon,
|
FilterIcon,
|
||||||
PreferencesIcon,
|
PreferencesIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
|
UpgradeIcon,
|
||||||
} from "./actions";
|
} from "./actions";
|
||||||
import { AddIcon } from "./actions/add-icon";
|
import { AddIcon } from "./actions/add-icon";
|
||||||
import { CloseIcon } from "./actions/close-icon";
|
import { CloseIcon } from "./actions/close-icon";
|
||||||
|
|
@ -134,6 +135,7 @@ export const ICON_REGISTRY = {
|
||||||
"action.search": SearchIcon,
|
"action.search": SearchIcon,
|
||||||
"action.preferences": PreferencesIcon,
|
"action.preferences": PreferencesIcon,
|
||||||
"action.copy-link": CopyLinkIcon,
|
"action.copy-link": CopyLinkIcon,
|
||||||
|
"action.upgrade": UpgradeIcon,
|
||||||
|
|
||||||
// Arrow icons
|
// Arrow icons
|
||||||
"arrow.chevron-down": ChevronDownIcon,
|
"arrow.chevron-down": ChevronDownIcon,
|
||||||
|
|
|
||||||
|
|
@ -112,3 +112,33 @@ export type SaturationCurve = "ease-in-out" | "linear";
|
||||||
* Default saturation curve
|
* Default saturation curve
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_SATURATION_CURVE: SaturationCurve = "ease-in-out";
|
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 { hexToOKLCH, oklchToCSS, getRelativeLuminance, getPerceptualBrightness } from "./color-conversion";
|
||||||
import type { OKLCH } 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 { generateThemePalettes } from "./palette-generator";
|
||||||
import { getBrandMapping, getNeutralMapping, invertPalette } from "./theme-inversion";
|
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");
|
const { textColor, iconColor } = getOnColorTextColors(brandColor, "wcag");
|
||||||
themeElement.style.setProperty(`--text-color-on-color`, oklchToCSS(textColor));
|
themeElement.style.setProperty(`--text-color-on-color`, oklchToCSS(textColor));
|
||||||
themeElement.style.setProperty(`--text-color-icon-on-color`, oklchToCSS(iconColor));
|
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-on-color`);
|
||||||
themeElement.style.removeProperty(`--text-color-icon-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…
Add table
Add a link
Reference in a new issue