diff --git a/src/components/InitialSyncGate.tsx b/src/components/InitialSyncGate.tsx
index 3430ac1d..3256b07b 100644
--- a/src/components/InitialSyncGate.tsx
+++ b/src/components/InitialSyncGate.tsx
@@ -227,8 +227,14 @@ const THEMES: ThemeOption[] = [
{ id: 'system', label: 'System', description: 'Matches your device', preview: '', splitPreview: true, builtinTheme: 'system' },
{ id: 'dark', label: 'Dark', description: 'Deep purple dark theme', preview: 'bg-[hsl(228,20%,10%)]', builtinTheme: 'dark' },
{ id: 'light', label: 'Light', description: 'Clean and bright', preview: 'bg-white border border-border', builtinTheme: 'light' },
- { id: 'black', label: 'Black', description: 'True OLED black', preview: 'bg-black', presetId: 'black' },
- { id: 'pink', label: 'Pink', description: 'Warm and playful', preview: 'bg-[hsl(330,100%,96%)]', presetId: 'pink' },
+ // Generate entries from all theme presets
+ ...Object.entries(themePresets).map(([id, preset]) => ({
+ id,
+ label: preset.label,
+ description: `${preset.emoji} ${preset.label} theme`,
+ preview: `bg-[hsl(${preset.tokens.background})]`,
+ presetId: id,
+ })),
];
interface ContentKind {
diff --git a/src/components/MobileBottomNav.tsx b/src/components/MobileBottomNav.tsx
index f8814ac6..9ef37cd0 100644
--- a/src/components/MobileBottomNav.tsx
+++ b/src/components/MobileBottomNav.tsx
@@ -1,6 +1,11 @@
import { useCallback, useMemo, useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
-import { Home, Compass, Bell, User, Search, TrendingUp, Bookmark, Clapperboard, BarChart3, Palette, PartyPopper, Radio, FileText } from 'lucide-react';
+import { Home, Compass, Bell, User, Search, Bookmark, TrendingUp, Clapperboard, BarChart3, Palette, PartyPopper, Radio, FileText, Pencil, GripVertical, X, Plus } from 'lucide-react';
+import LoginDialog from '@/components/auth/LoginDialog';
+import { useOnboarding } from '@/components/InitialSyncGate';
+import { DndContext, closestCenter, TouchSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core';
+import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
+import { CSS } from '@dnd-kit/utilities';
import { Drawer, DrawerContent, DrawerTitle } from '@/components/ui/drawer';
import { ChestIcon } from '@/components/icons/ChestIcon';
import { CardsIcon } from '@/components/icons/CardsIcon';
@@ -90,21 +95,95 @@ function NavTab({ icon, label, active, showIndicator, onClick, to }: NavTabProps
);
}
+// ── Sortable explore item (edit mode) ─────────────────────────────────────────
+
+interface SortableExploreSheetItemProps {
+ id: string;
+ onRemove: (id: string) => void;
+}
+
+function SortableExploreSheetItem({ id, onRemove }: SortableExploreSheetItemProps) {
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id });
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ };
+
+ const icon = ITEM_ICONS[id] ?? ;
+ const label = itemLabel(id);
+
+ return (
+
+
+
+
+ {icon}
+ {label}
+
+
+
+
+ );
+}
+
// ── Main component ────────────────────────────────────────────────────────────
export function MobileBottomNav() {
const location = useLocation();
const { user, metadata } = useCurrentUser();
const hasUnread = useHasUnreadNotifications();
- const { orderedItems } = useFeedSettings();
+ const {
+ orderedItems, hiddenItems, updateSidebarOrder, addToSidebar, removeFromSidebar,
+ } = useFeedSettings();
const userProfileUrl = useProfileUrl(user?.pubkey ?? '', metadata);
+ const { startSignup } = useOnboarding();
const [exploreOpen, setExploreOpen] = useState(false);
+ const [editing, setEditing] = useState(false);
+ const [loginDialogOpen, setLoginDialogOpen] = useState(false);
- const handleHomeClick = useCallback(() => {
- if (location.pathname === '/') {
- window.scrollTo({ top: 0, behavior: 'smooth' });
- }
- }, [location.pathname]);
+ // DnD sensors — touch sensor with delay to distinguish scroll from drag
+ const sensors = useSensors(
+ useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 5 } }),
+ useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
+ );
+
+ const handleDragEnd = useCallback((event: DragEndEvent) => {
+ const { active, over } = event;
+ if (!over || active.id === over.id) return;
+
+ const oldIndex = orderedItems.indexOf(active.id as string);
+ const newIndex = orderedItems.indexOf(over.id as string);
+ if (oldIndex === -1 || newIndex === -1) return;
+
+ const newOrder = arrayMove(orderedItems, oldIndex, newIndex);
+ updateSidebarOrder(newOrder);
+ }, [orderedItems, updateSidebarOrder]);
// Build explore items from ordered items (includes built-ins)
const exploreItems = useMemo(() => {
@@ -116,88 +195,162 @@ export function MobileBottomNav() {
}));
}, [orderedItems]);
- // Check if current path matches any explore route (excluding __feed which is the Home tab)
+ // Check if current path matches any explore route
const isExploreActive = orderedItems.some((id) =>
- id !== '__feed' && isItemActive(id, location.pathname, location.search),
+ isItemActive(id, location.pathname, location.search),
);
+ const handleDrawerClose = (open: boolean) => {
+ if (!open) setEditing(false);
+ setExploreOpen(open);
+ };
+
return (
<>
{/* Explore bottom sheet */}
-
+
Explore
-
- Explore
-
- {exploreItems.length > 0 ? (
-
- {exploreItems.map((item) => (
-
setExploreOpen(false)}
- className={cn(
- 'flex items-center gap-3 px-4 py-3.5 rounded-xl transition-colors',
- isItemActive(item.id, location.pathname, location.search)
- ? 'text-accent font-semibold'
- : 'text-foreground hover:bg-secondary/60',
- )}
+ {/* Section header with edit toggle */}
+
+
+ Explore
+
+
+
+
+
+ {editing ? (
+ /* ── Edit mode: single-column sortable list ── */
+ <>
+
+
- {item.icon}
- {item.label}
-
- ))}
-
+ {orderedItems.map((id) => (
+
+ ))}
+
+
+
+ {/* Add hidden items back */}
+ {hiddenItems.length > 0 && (
+
+ {hiddenItems.map((item) => (
+
+ ))}
+
+ )}
+ >
) : (
-
- No content sections enabled. Go to Settings to add some.
-
+ /* ── Normal mode: 2-column grid of links ── */
+ exploreItems.length > 0 ? (
+
+ {exploreItems.map((item) => (
+ setExploreOpen(false)}
+ className={cn(
+ 'flex items-center gap-3 px-4 py-3.5 rounded-xl transition-colors',
+ isItemActive(item.id, location.pathname, location.search)
+ ? 'text-accent font-semibold'
+ : 'text-foreground hover:bg-secondary/60',
+ )}
+ >
+ {item.icon}
+ {item.label}
+
+ ))}
+
+ ) : (
+
+ No content sections enabled. Tap the pencil to add some.
+
+ )
)}
+
+ {/* Login dialog for logged-out "You" tab */}
+ setLoginDialogOpen(false)}
+ onLogin={() => setLoginDialogOpen(false)}
+ onSignupClick={startSignup}
+ />
>
);
}
diff --git a/src/components/MobileDrawer.tsx b/src/components/MobileDrawer.tsx
index 9b007dcc..3d7706ac 100644
--- a/src/components/MobileDrawer.tsx
+++ b/src/components/MobileDrawer.tsx
@@ -1,144 +1,22 @@
-import { Link, useLocation, useNavigate } from 'react-router-dom';
-import { useUserThemes } from '@/hooks/useUserThemes';
-import { Home, TrendingUp, Bookmark, Settings, LogOut, ChevronDown, ChevronUp, Sun, Moon, Monitor, Clapperboard, BarChart3, Palette, PartyPopper, Radio, FileText, Pencil, GripVertical, X, Plus } from 'lucide-react';
-import { DndContext, closestCenter, TouchSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core';
-import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
-import { CSS } from '@dnd-kit/utilities';
+import { Link, useNavigate } from 'react-router-dom';
+import { LogOut, ChevronDown, ChevronUp, Sun, Moon, Monitor, Palette } from 'lucide-react';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { Sheet, SheetContent, SheetTitle } from '@/components/ui/sheet';
import { Separator } from '@/components/ui/separator';
-import { ChestIcon } from '@/components/icons/ChestIcon';
-import { CardsIcon } from '@/components/icons/CardsIcon';
import { DittoLogo } from '@/components/DittoLogo';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useLoginActions } from '@/hooks/useLoginActions';
import { useLoggedInAccounts } from '@/hooks/useLoggedInAccounts';
import { useTheme } from '@/hooks/useTheme';
-import { useFeedSettings, getBuiltinItem } from '@/hooks/useFeedSettings';
-import { EXTRA_KINDS } from '@/lib/extraKinds';
+import { useUserThemes } from '@/hooks/useUserThemes';
import { LoginArea } from '@/components/auth/LoginArea';
import { genUserName } from '@/lib/genUserName';
-import { useCallback, useMemo, useState } from 'react';
+import { useMemo, useState } from 'react';
import type { Theme } from '@/contexts/AppContext';
import { themePresets, type ThemeTokens } from '@/themes';
-import { cn } from '@/lib/utils';
+import { settingsSections, type SettingsSection } from '@/pages/SettingsPage';
-/** Map item ID to icon for drawer items. Covers both built-ins and extra-kind routes. */
-const ITEM_ICONS: Record = {
- __feed: ,
- __trends: ,
- __bookmarks: ,
- vines: ,
- polls: ,
- treasures: ,
- colors: ,
- packs: ,
- streams: ,
- articles: ,
- decks: ,
-};
-
-function itemLabel(id: string): string {
- const builtin = getBuiltinItem(id);
- if (builtin) return builtin.label;
- return EXTRA_KINDS.find((d) => d.route === id)?.label ?? id;
-}
-
-function itemPath(id: string): string {
- const builtin = getBuiltinItem(id);
- if (builtin) return builtin.path;
- return `/${id}`;
-}
-
-// ── Drawer menu item (normal mode) ───────────────────────────────────────────
-
-interface DrawerMenuItemProps {
- to: string;
- icon: React.ReactNode;
- label: string;
- onClick: () => void;
-}
-
-function DrawerMenuItem({ to, icon, label, onClick }: DrawerMenuItemProps) {
- const location = useLocation();
- const active = location.pathname === to;
-
- return (
-
- {icon}
- {label}
-
- );
-}
-
-// ── Sortable drawer item (edit mode) ─────────────────────────────────────────
-
-interface SortableDrawerItemProps {
- id: string;
- onRemove: (id: string) => void;
-}
-
-function SortableDrawerItem({ id, onRemove }: SortableDrawerItemProps) {
- const {
- attributes,
- listeners,
- setNodeRef,
- transform,
- transition,
- isDragging,
- } = useSortable({ id });
-
- const style = {
- transform: CSS.Transform.toString(transform),
- transition,
- };
-
- const icon = ITEM_ICONS[id] ?? ;
- const label = itemLabel(id);
-
- return (
-
- {/* Drag handle */}
-
-
- {/* Icon + label (non-interactive in edit mode) */}
-
- {icon}
- {label}
-
-
- {/* Remove button */}
-
-
- );
-}
-
-// ── Mobile drawer props ──────────────────────────────────────────────────────
+// ── Mobile drawer ────────────────────────────────────────────────────────────
interface MobileDrawerProps {
open: boolean;
@@ -150,41 +28,13 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) {
const { logout } = useLoginActions();
const { otherUsers, setLogin } = useLoggedInAccounts();
const { theme, setTheme, applyCustomTheme, customTheme } = useTheme();
- const {
- orderedItems, hiddenItems, updateSidebarOrder, addToSidebar, removeFromSidebar,
- } = useFeedSettings();
const navigate = useNavigate();
const [showAccountSwitcher, setShowAccountSwitcher] = useState(false);
- const [editing, setEditing] = useState(false);
- // DnD sensors — touch sensor with delay to distinguish scroll from drag
- const sensors = useSensors(
- useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 5 } }),
- useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
- );
-
- /** Build explore items from ordered items (includes built-ins). */
- const exploreItems = useMemo(() => {
- return orderedItems.map((id) => ({
- id,
- to: itemPath(id),
- icon: ITEM_ICONS[id] ?? ,
- label: itemLabel(id),
- }));
- }, [orderedItems]);
-
- /** Handle drag-and-drop reorder. */
- const handleDragEnd = useCallback((event: DragEndEvent) => {
- const { active, over } = event;
- if (!over || active.id === over.id) return;
-
- const oldIndex = orderedItems.indexOf(active.id as string);
- const newIndex = orderedItems.indexOf(over.id as string);
- if (oldIndex === -1 || newIndex === -1) return;
-
- const newOrder = arrayMove(orderedItems, oldIndex, newIndex);
- updateSidebarOrder(newOrder);
- }, [orderedItems, updateSidebarOrder]);
+ /** Settings sections filtered by auth state. */
+ const visibleSettings = useMemo(() => {
+ return settingsSections.filter((s) => !s.requiresAuth || user);
+ }, [user]);
// Theme cycling logic
const builtinCycle: { id: Theme; label: string; icon: React.ReactNode }[] = [
@@ -268,10 +118,7 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) {
}
};
- const handleClose = () => {
- setEditing(false);
- onOpenChange(false);
- };
+ const handleClose = () => onOpenChange(false);
const handleLogout = async () => {
await logout();
@@ -280,8 +127,8 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) {
};
return (
- { if (!v) setEditing(false); onOpenChange(v); }}>
-
+
+
Navigation menu
{user ? (
@@ -297,96 +144,32 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) {
{/* Menu items */}