Restructure theme selector: My Themes section at top with Create/Edit, presets below

- 'My Themes' section at top with 'Create Theme' card + user's published themes
- Clicking a user theme applies it AND navigates to the builder to edit it (?edit=identifier)
- 'Browse Public Themes' card in My Themes section
- Presets section below with System/Light/Dark + all preset themes
- Removed 'Customize your own theme' bottom link (replaced by Create Theme card)
- Theme builder handles ?edit= param to load the correct theme with editing context
This commit is contained in:
Mary Kate Fain
2026-02-25 16:37:58 -06:00
parent 373dd7a337
commit 0774ee2ac5
2 changed files with 107 additions and 66 deletions
+94 -65
View File
@@ -1,5 +1,5 @@
import { Check, Paintbrush, Globe } from 'lucide-react';
import { Link } from 'react-router-dom';
import { Check, Globe, Plus } from 'lucide-react';
import { Link, useNavigate } from 'react-router-dom';
import { type Theme } from '@/contexts/AppContext';
import { useTheme } from '@/hooks/useTheme';
import { useCurrentUser } from '@/hooks/useCurrentUser';
@@ -78,6 +78,9 @@ export function ThemeSelector() {
const { theme, customTheme, setTheme, applyCustomTheme } = useTheme();
const { user } = useCurrentUser();
const userThemesQuery = useUserThemes(user?.pubkey);
const navigate = useNavigate();
const hasUserThemes = (userThemesQuery.data?.length ?? 0) > 0;
const builtinOptions: { id: Theme; label: string }[] = [
{ id: 'system', label: 'System' },
@@ -98,8 +101,79 @@ export function ThemeSelector() {
};
return (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-3">
<div className="space-y-5">
{/* ── My Themes section ── */}
{user && (
<div className="space-y-2">
<h3 className="text-xs font-semibold uppercase tracking-wider text-accent/70">My Themes</h3>
<div className="grid grid-cols-3 gap-3">
{/* Create new custom theme */}
<Link
to="/settings/theme"
className="relative group rounded-xl border-2 border-dashed p-1 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring border-border hover:border-accent/40"
>
<div className="aspect-[4/3] rounded-lg overflow-hidden relative flex flex-col items-center justify-center gap-1.5 bg-muted/30">
<Plus className="size-5 text-muted-foreground group-hover:text-accent transition-colors" />
<span className="text-[10px] text-muted-foreground group-hover:text-accent transition-colors font-medium">New</span>
</div>
<p className="mt-1.5 text-xs font-medium text-center text-muted-foreground group-hover:text-foreground transition-colors">
Create Theme
</p>
</Link>
{/* User's published themes */}
{userThemesQuery.data?.map((userTheme) => {
const isActive = theme === 'custom' && customTheme && JSON.stringify(customTheme) === JSON.stringify(userTheme.tokens);
return (
<button
key={`user-${userTheme.identifier}`}
className={cn(
'relative group rounded-xl border-2 p-1 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring text-left',
isActive
? 'border-primary shadow-sm'
: 'border-border hover:border-primary/40',
)}
onClick={() => {
applyCustomTheme(userTheme.tokens);
navigate(`/settings/theme?edit=${userTheme.identifier}`);
}}
>
<ThemePreviewCard tokens={userTheme.tokens} isActive={!!isActive} />
<p className={cn(
'mt-1.5 text-xs font-medium text-center transition-colors truncate',
isActive ? 'text-foreground' : 'text-muted-foreground',
)}>
{userTheme.title}
</p>
</button>
);
})}
{/* Browse public themes */}
<Link
to="/themes"
className="relative group rounded-xl border-2 border-dashed p-1 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring border-border hover:border-primary/40"
>
<div className="aspect-[4/3] rounded-lg overflow-hidden relative flex flex-col items-center justify-center gap-1.5 bg-muted/30">
<Globe className="size-5 text-muted-foreground group-hover:text-primary transition-colors" />
<span className="text-[10px] text-muted-foreground group-hover:text-primary transition-colors font-medium">Browse</span>
</div>
<p className="mt-1.5 text-xs font-medium text-center text-muted-foreground group-hover:text-foreground transition-colors">
Public Themes
</p>
</Link>
</div>
</div>
)}
{/* ── Presets section ── */}
<div className="space-y-2">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground/70">
{hasUserThemes ? 'Presets' : 'Themes'}
</h3>
<div className="grid grid-cols-3 gap-3">
{builtinOptions.map((option) => {
if (option.id === 'system') {
const isActive = theme === 'system';
@@ -195,19 +269,6 @@ export function ThemeSelector() {
);
})}
{/* Active custom theme (if it doesn't match any preset) */}
{theme === 'custom' && customTheme && !presetOptions.some(p => isPresetActive(p.tokens)) && (
<button
key="custom-active"
className="relative group rounded-xl border-2 p-1 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring border-primary shadow-sm"
>
<ThemePreviewCard tokens={customTheme} isActive />
<p className="mt-1.5 text-xs font-medium text-center transition-colors text-foreground">
Custom
</p>
</button>
)}
{/* Preset buttons */}
{presetOptions.map((preset) => {
const isActive = isPresetActive(preset.tokens);
@@ -234,55 +295,23 @@ export function ThemeSelector() {
);
})}
{/* User's published themes */}
{userThemesQuery.data?.map((userTheme) => {
const isActive = theme === 'custom' && customTheme && JSON.stringify(customTheme) === JSON.stringify(userTheme.tokens);
return (
<button
key={`user-${userTheme.identifier}`}
className={cn(
'relative group rounded-xl border-2 p-1 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
isActive
? 'border-primary shadow-sm'
: 'border-border hover:border-primary/40',
)}
onClick={() => applyCustomTheme(userTheme.tokens)}
>
<ThemePreviewCard tokens={userTheme.tokens} isActive={!!isActive} />
<p className={cn(
'mt-1.5 text-xs font-medium text-center transition-colors truncate',
isActive ? 'text-foreground' : 'text-muted-foreground',
)}>
{userTheme.title}
</p>
</button>
);
})}
{/* Browse public themes */}
<Link
to="/themes"
className="relative group rounded-xl border-2 border-dashed p-1 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring border-border hover:border-primary/40"
>
<div className="aspect-[4/3] rounded-lg overflow-hidden relative flex flex-col items-center justify-center gap-1.5 bg-muted/30">
<Globe className="size-5 text-muted-foreground group-hover:text-primary transition-colors" />
<span className="text-[10px] text-muted-foreground group-hover:text-primary transition-colors font-medium">Browse</span>
</div>
<p className="mt-1.5 text-xs font-medium text-center text-muted-foreground group-hover:text-foreground transition-colors">
Public Themes
</p>
</Link>
{/* Browse public themes — shown in presets section when user has no custom themes */}
{!user && (
<Link
to="/themes"
className="relative group rounded-xl border-2 border-dashed p-1 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring border-border hover:border-primary/40"
>
<div className="aspect-[4/3] rounded-lg overflow-hidden relative flex flex-col items-center justify-center gap-1.5 bg-muted/30">
<Globe className="size-5 text-muted-foreground group-hover:text-primary transition-colors" />
<span className="text-[10px] text-muted-foreground group-hover:text-primary transition-colors font-medium">Browse</span>
</div>
<p className="mt-1.5 text-xs font-medium text-center text-muted-foreground group-hover:text-foreground transition-colors">
Public Themes
</p>
</Link>
)}
</div>
{/* Customize link */}
<Link
to="/settings/theme"
className="flex items-center justify-center gap-2 w-full py-2.5 rounded-lg border border-dashed border-border text-sm font-medium text-muted-foreground hover:text-foreground hover:border-primary/40 transition-colors"
>
<Paintbrush className="size-4" />
Customize your own theme
</Link>
</div>
</div>
);
}
+13 -1
View File
@@ -95,9 +95,10 @@ export function ThemeBuilderPage() {
const { user } = useCurrentUser();
const { publishTheme, setActiveTheme, deleteTheme, isPending: isPublishing } = usePublishTheme();
// Check if we're importing from a profile
// Check if we're importing from a profile or editing an existing theme
const importPubkey = searchParams.get('import');
const importThemeId = searchParams.get('theme');
const editIdentifier = searchParams.get('edit');
// Check if the user currently has a published active profile theme
const ownActiveTheme = useActiveProfileTheme(user?.pubkey);
@@ -135,6 +136,17 @@ export function ThemeBuilderPage() {
if (match) setActiveEditingTheme(match);
}, [_userThemes.data]); // eslint-disable-line react-hooks/exhaustive-deps
// Load a specific theme for editing via ?edit= param
useEffect(() => {
if (!editIdentifier || !_userThemes.data) return;
const target = _userThemes.data.find(t => t.identifier === editIdentifier);
if (target && activeEditingTheme?.identifier !== editIdentifier) {
setTokens(target.tokens);
setAutoDerive(false);
setActiveEditingTheme(target);
}
}, [editIdentifier, _userThemes.data]); // eslint-disable-line react-hooks/exhaustive-deps
// Clear editing context when starting fresh
const handleNewTheme = useCallback(() => {
setActiveEditingTheme(null);