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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user