feat(web): improve theme system clarity and fix login page theme selection

- Add descriptive labels to distinguish workspace vs user theme settings
- Fix login page theme selection not reflecting in dropdown
- Add ThemeInfoCard component explaining theme system
- Enhance ThemeSelect with effective theme indicators
- Improve UX by clarifying theme scope and priority

Resolves theme confusion between System > General > Theme and System > Basic > Edit > Theme
pull/5203/head
Omid Saadat 1 week ago
parent d794c0bf8b
commit 279edf8b6f

@ -1,6 +1,8 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useState } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { workspaceStore } from "@/store"; import { workspaceStore } from "@/store";
import { loadTheme } from "@/utils/theme";
import LocaleSelect from "./LocaleSelect"; import LocaleSelect from "./LocaleSelect";
import ThemeSelect from "./ThemeSelect"; import ThemeSelect from "./ThemeSelect";
@ -9,10 +11,22 @@ interface Props {
} }
const AuthFooter = observer(({ className }: Props) => { const AuthFooter = observer(({ className }: Props) => {
// Local state for login page theme since we can't persist to server
const [localTheme, setLocalTheme] = useState(workspaceStore.state.theme || "default");
const handleThemeChange = (theme: string) => {
// Update local state
setLocalTheme(theme);
// Update workspace store for immediate UI feedback
workspaceStore.state.setPartial({ theme });
// Apply theme to DOM
loadTheme(theme);
};
return ( return (
<div className={cn("mt-4 flex flex-row items-center justify-center w-full gap-2", className)}> <div className={cn("mt-4 flex flex-row items-center justify-center w-full gap-2", className)}>
<LocaleSelect value={workspaceStore.state.locale} onChange={(locale) => workspaceStore.state.setPartial({ locale })} /> <LocaleSelect value={workspaceStore.state.locale} onChange={(locale) => workspaceStore.state.setPartial({ locale })} />
<ThemeSelect value={workspaceStore.state.theme} onValueChange={(theme) => workspaceStore.state.setPartial({ theme })} /> <ThemeSelect value={localTheme} onValueChange={handleThemeChange} />
</div> </div>
); );
}); });

@ -8,6 +8,7 @@ import { useTranslate } from "@/utils/i18n";
import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/memo"; import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/memo";
import LocaleSelect from "../LocaleSelect"; import LocaleSelect from "../LocaleSelect";
import ThemeSelect from "../ThemeSelect"; import ThemeSelect from "../ThemeSelect";
import ThemeInfoCard from "../ThemeInfoCard";
import VisibilityIcon from "../VisibilityIcon"; import VisibilityIcon from "../VisibilityIcon";
import WebhookSection from "./WebhookSection"; import WebhookSection from "./WebhookSection";
@ -47,10 +48,15 @@ const PreferencesSection = observer(() => {
</div> </div>
<div className="w-full flex flex-row justify-between items-center"> <div className="w-full flex flex-row justify-between items-center">
<div className="flex flex-col">
<span>{t("setting.preference-section.theme")}</span> <span>{t("setting.preference-section.theme")}</span>
<span className="text-xs text-muted-foreground">Your personal theme preference (overrides default)</span>
</div>
<ThemeSelect value={setting.theme} onValueChange={handleThemeChange} /> <ThemeSelect value={setting.theme} onValueChange={handleThemeChange} />
</div> </div>
<ThemeInfoCard />
<p className="font-medium text-muted-foreground">{t("setting.preference")}</p> <p className="font-medium text-muted-foreground">{t("setting.preference")}</p>
<div className="w-full flex flex-row justify-between items-center"> <div className="w-full flex flex-row justify-between items-center">

@ -81,7 +81,10 @@ const WorkspaceSection = observer(() => {
<Separator /> <Separator />
<p className="font-medium text-foreground">{t("setting.system-section.title")}</p> <p className="font-medium text-foreground">{t("setting.system-section.title")}</p>
<div className="w-full flex flex-row justify-between items-center"> <div className="w-full flex flex-row justify-between items-center">
<span>Theme</span> <div className="flex flex-col">
<span>Default Theme</span>
<span className="text-xs text-muted-foreground">Sets the default theme for all users</span>
</div>
<ThemeSelect <ThemeSelect
value={workspaceGeneralSetting.theme || "default"} value={workspaceGeneralSetting.theme || "default"}
onValueChange={(value: string) => updatePartialSetting({ theme: value })} onValueChange={(value: string) => updatePartialSetting({ theme: value })}

@ -0,0 +1,33 @@
import { observer } from "mobx-react-lite";
import { Info } from "lucide-react";
import { userStore, workspaceStore } from "@/store";
const ThemeInfoCard = observer(() => {
const userTheme = userStore.state.userGeneralSetting?.theme;
const workspaceTheme = workspaceStore.state.theme || "default";
const effectiveTheme = userTheme || workspaceTheme;
return (
<div className="border border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950 rounded-lg p-4">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5" />
<div className="space-y-2">
<h4 className="font-medium text-blue-900 dark:text-blue-100">Theme System</h4>
<div className="text-sm text-blue-800 dark:text-blue-200 space-y-1">
<p><strong>Current effective theme:</strong> {effectiveTheme}</p>
<p><strong>Workspace default:</strong> {workspaceTheme}</p>
{userTheme && (
<p><strong>Your preference:</strong> {userTheme}</p>
)}
<p className="text-xs mt-2">
Your personal theme preference overrides the workspace default.
If you haven't set a personal preference, the workspace default is used.
</p>
</div>
</div>
</div>
</div>
);
});
export default ThemeInfoCard;

@ -1,12 +1,14 @@
import { observer } from "mobx-react-lite";
import { Moon, Palette, Sun, Wallpaper } from "lucide-react"; import { Moon, Palette, Sun, Wallpaper } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { workspaceStore } from "@/store"; import { workspaceStore, userStore } from "@/store";
import { THEME_OPTIONS } from "@/utils/theme"; import { THEME_OPTIONS } from "@/utils/theme";
interface ThemeSelectProps { interface ThemeSelectProps {
value?: string; value?: string;
onValueChange?: (theme: string) => void; onValueChange?: (theme: string) => void;
className?: string; className?: string;
showEffectiveTheme?: boolean;
} }
const THEME_ICONS: Record<string, JSX.Element> = { const THEME_ICONS: Record<string, JSX.Element> = {
@ -16,9 +18,14 @@ const THEME_ICONS: Record<string, JSX.Element> = {
whitewall: <Wallpaper className="w-4 h-4" />, whitewall: <Wallpaper className="w-4 h-4" />,
}; };
const ThemeSelect = ({ value, onValueChange, className }: ThemeSelectProps = {}) => { const ThemeSelect = observer(({ value, onValueChange, className, showEffectiveTheme = false }: ThemeSelectProps = {}) => {
const currentTheme = value || workspaceStore.state.theme || "default"; const currentTheme = value || workspaceStore.state.theme || "default";
// Calculate effective theme (user preference overrides workspace default)
const effectiveTheme = userStore.state.userGeneralSetting?.theme || workspaceStore.state.theme || "default";
const displayTheme = showEffectiveTheme ? effectiveTheme : currentTheme;
const handleThemeChange = (newTheme: Theme) => { const handleThemeChange = (newTheme: Theme) => {
if (onValueChange) { if (onValueChange) {
onValueChange(newTheme); onValueChange(newTheme);
@ -32,6 +39,9 @@ const ThemeSelect = ({ value, onValueChange, className }: ThemeSelectProps = {})
<SelectTrigger className={className}> <SelectTrigger className={className}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SelectValue placeholder="Select theme" /> <SelectValue placeholder="Select theme" />
{showEffectiveTheme && effectiveTheme !== currentTheme && (
<span className="text-xs text-muted-foreground">(effective: {effectiveTheme})</span>
)}
</div> </div>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -40,12 +50,15 @@ const ThemeSelect = ({ value, onValueChange, className }: ThemeSelectProps = {})
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{THEME_ICONS[option.value]} {THEME_ICONS[option.value]}
<span>{option.label}</span> <span>{option.label}</span>
{showEffectiveTheme && option.value === effectiveTheme && (
<span className="text-xs text-green-600"></span>
)}
</div> </div>
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
); );
}; });
export default ThemeSelect; export default ThemeSelect;

Loading…
Cancel
Save