fix: Improve theme contrast and persistence

- Fix theme persistence: properly check localStorage before using default
- Plan9: make blue subtler (reduce saturation), darken gradient colors
  for better contrast on pale yellow background
- Light theme: improve contrast with darker muted foreground and borders
- Change theme selector from flat list to dropdown submenu
This commit is contained in:
Claude
2026-01-14 17:09:00 +00:00
parent 8c642eb91c
commit aaa916fedf
4 changed files with 97 additions and 93 deletions

View File

@@ -13,6 +13,9 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
} from "@/components/ui/dropdown-menu";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import Nip05 from "./nip05";
@@ -155,33 +158,30 @@ export default function UserMenu() {
)}
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel className="text-xs text-muted-foreground font-normal flex items-center gap-1.5">
<Palette className="size-3.5" />
<span>Theme</span>
</DropdownMenuLabel>
{availableThemes.map((theme) => (
<DropdownMenuItem
key={theme.id}
className="cursor-crosshair"
onClick={() => setTheme(theme.id)}
>
<span
className={`size-3 rounded-full mr-2 ${
themeId === theme.id
? "bg-primary"
: "bg-muted-foreground/30"
}`}
/>
<span className="text-sm">{theme.name}</span>
{themeId === theme.id && (
<span className="ml-auto text-xs text-muted-foreground">
active
</span>
)}
</DropdownMenuItem>
))}
</DropdownMenuGroup>
<DropdownMenuSub>
<DropdownMenuSubTrigger className="cursor-crosshair">
<Palette className="size-4 mr-2" />
Theme
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{availableThemes.map((theme) => (
<DropdownMenuItem
key={theme.id}
className="cursor-crosshair"
onClick={() => setTheme(theme.id)}
>
<span
className={`size-2 rounded-full mr-2 ${
themeId === theme.id
? "bg-primary"
: "bg-muted-foreground/30"
}`}
/>
{theme.name}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className="cursor-crosshair">
Log out
@@ -189,33 +189,30 @@ export default function UserMenu() {
</>
) : (
<>
<DropdownMenuGroup>
<DropdownMenuLabel className="text-xs text-muted-foreground font-normal flex items-center gap-1.5">
<Palette className="size-3.5" />
<span>Theme</span>
</DropdownMenuLabel>
{availableThemes.map((theme) => (
<DropdownMenuItem
key={theme.id}
className="cursor-crosshair"
onClick={() => setTheme(theme.id)}
>
<span
className={`size-3 rounded-full mr-2 ${
themeId === theme.id
? "bg-primary"
: "bg-muted-foreground/30"
}`}
/>
<span className="text-sm">{theme.name}</span>
{themeId === theme.id && (
<span className="ml-auto text-xs text-muted-foreground">
active
</span>
)}
</DropdownMenuItem>
))}
</DropdownMenuGroup>
<DropdownMenuSub>
<DropdownMenuSubTrigger className="cursor-crosshair">
<Palette className="size-4 mr-2" />
Theme
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{availableThemes.map((theme) => (
<DropdownMenuItem
key={theme.id}
className="cursor-crosshair"
onClick={() => setTheme(theme.id)}
>
<span
className={`size-2 rounded-full mr-2 ${
themeId === theme.id
? "bg-primary"
: "bg-muted-foreground/30"
}`}
/>
{theme.name}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setShowLogin(true)}>
Log in

View File

@@ -25,23 +25,23 @@ export const lightTheme: Theme = {
secondary: "210 40% 96.1%",
secondaryForeground: "222.2 47.4% 11.2%",
accent: "270 80% 60%",
accent: "270 70% 55%",
accentForeground: "0 0% 100%",
muted: "210 40% 96.1%",
mutedForeground: "215.4 16.3% 46.9%",
mutedForeground: "215.4 16.3% 40%",
destructive: "0 84.2% 60.2%",
destructiveForeground: "210 40% 98%",
destructive: "0 72% 51%",
destructiveForeground: "0 0% 100%",
border: "214.3 31.8% 91.4%",
border: "214.3 31.8% 85%",
input: "214.3 31.8% 91.4%",
ring: "222.2 84% 4.9%",
// Status colors
success: "142 76% 36%",
warning: "45 93% 47%",
info: "199 89% 48%",
// Status colors (darker for better contrast)
success: "142 70% 30%",
warning: "38 92% 40%",
info: "199 80% 40%",
},
syntax: {

View File

@@ -27,9 +27,9 @@ export const plan9Theme: Theme = {
popover: "60 100% 97%",
popoverForeground: "0 0% 0%",
// Dark blue for interactive elements
primary: "220 100% 25%",
primaryForeground: "60 100% 94%",
// Muted blue for interactive elements (subtler than before)
primary: "220 50% 35%",
primaryForeground: "60 100% 96%",
// Muted green secondary
secondary: "120 30% 88%",
@@ -41,54 +41,54 @@ export const plan9Theme: Theme = {
// Muted yellow for subdued elements
muted: "60 30% 88%",
mutedForeground: "0 0% 35%",
mutedForeground: "0 0% 25%",
// Red for destructive
destructive: "0 70% 45%",
destructive: "0 70% 40%",
destructiveForeground: "0 0% 100%",
// Dark blue borders
border: "220 40% 50%",
// Subtle borders
border: "60 20% 75%",
input: "60 30% 92%",
ring: "220 100% 25%",
ring: "220 50% 35%",
// Status colors
success: "120 60% 30%",
warning: "45 90% 45%",
info: "200 80% 40%",
// Status colors (darker for contrast)
success: "120 60% 25%",
warning: "35 90% 35%",
info: "200 70% 35%",
},
syntax: {
// Acme-inspired syntax colors
comment: "0 0% 45%", // Gray
punctuation: "0 0% 25%", // Dark gray
property: "220 100% 25%", // Dark blue
property: "220 50% 35%", // Muted blue
string: "120 60% 28%", // Forest green
keyword: "280 60% 35%", // Purple
function: "220 100% 25%", // Dark blue
keyword: "280 50% 35%", // Muted purple
function: "220 50% 35%", // Muted blue
variable: "0 0% 0%", // Black
operator: "0 0% 15%", // Near black
// Diff colors - subtle on yellow background
diffInserted: "120 60% 25%",
diffInsertedBg: "120 50% 85%",
diffDeleted: "0 65% 40%",
diffDeletedBg: "0 50% 90%",
diffMeta: "200 70% 35%",
diffMetaBg: "200 50% 88%",
diffInsertedBg: "120 40% 85%",
diffDeleted: "0 60% 40%",
diffDeletedBg: "0 40% 90%",
diffMeta: "200 50% 35%",
diffMetaBg: "200 40% 88%",
},
scrollbar: {
thumb: "220 30% 55%",
thumbHover: "220 40% 45%",
track: "60 30% 90%",
thumb: "60 20% 70%",
thumbHover: "60 25% 60%",
track: "60 30% 92%",
},
gradient: {
// Muted gradient for Plan9 aesthetic
color1: "180 140 20", // Olive/mustard
color2: "200 120 50", // Burnt orange
color3: "100 60 180", // Muted purple
color4: "40 160 180", // Teal
// Darker gradient for contrast on pale yellow background
color1: "140 110 20", // Dark olive/mustard
color2: "180 90 40", // Dark burnt orange
color3: "80 50 140", // Dark muted purple
color4: "30 120 130", // Dark teal
},
};

View File

@@ -108,9 +108,16 @@ export function ThemeProvider({
children,
defaultTheme,
}: ThemeProviderProps): React.ReactElement {
// Initialize from localStorage or default
// Initialize from localStorage, falling back to defaultTheme prop or DEFAULT_THEME_ID
const [themeId, setThemeIdState] = React.useState<string>(() => {
return defaultTheme || getSavedThemeId();
const saved = getSavedThemeId();
// Only use defaultTheme if nothing is saved (saved returns DEFAULT_THEME_ID when empty)
// Check localStorage directly to see if user has explicitly chosen a theme
const hasExplicitSave = localStorage.getItem(STORAGE_KEY) !== null;
if (hasExplicitSave) {
return saved;
}
return defaultTheme || DEFAULT_THEME_ID;
});
const [customThemes, setCustomThemes] = React.useState<Theme[]>(() => {