diff --git a/package-lock.json b/package-lock.json
index 70ee4074..69b76ec8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3159,6 +3159,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3172,6 +3173,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3185,6 +3187,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3198,6 +3201,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3211,6 +3215,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3224,6 +3229,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3237,6 +3243,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3250,6 +3257,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3263,6 +3271,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3276,6 +3285,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3289,6 +3299,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3302,6 +3313,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3315,6 +3327,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3328,6 +3341,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3341,6 +3355,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3354,6 +3369,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3367,6 +3383,7 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3380,6 +3397,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3393,6 +3411,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3406,6 +3425,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3419,6 +3439,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3432,6 +3453,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3445,6 +3467,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3458,6 +3481,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3471,6 +3495,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
diff --git a/src/components/Feed.tsx b/src/components/Feed.tsx
index 37ad9b78..c756ed8a 100644
--- a/src/components/Feed.tsx
+++ b/src/components/Feed.tsx
@@ -24,7 +24,7 @@ import { useResolveTabFilter } from '@/hooks/useResolveTabFilter';
import { getEnabledFeedKinds } from '@/lib/extraKinds';
import { isRepostKind } from '@/lib/feedUtils';
import { isEventMuted } from '@/lib/muteHelpers';
-import { FeedTabButton } from '@/components/FeedTabButton';
+import { TabButton } from '@/components/TabButton';
import { DITTO_RELAY } from '@/lib/appRelays';
import type { FeedItem } from '@/lib/feedUtils';
import type { NostrEvent } from '@nostrify/nostrify';
@@ -230,18 +230,18 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage, fee
{/* Tabs (logged in) or CTA (logged out, main feed only) */}
{user ? (
-
handleSetActiveTab('follows')} />
+ handleSetActiveTab('follows')} />
{!isKindSpecificPage && showDittoFeed && (
- handleSetActiveTab('ditto')} />
+ handleSetActiveTab('ditto')} />
)}
{!isKindSpecificPage && showCommunityFeed && (
- handleSetActiveTab('communities')} />
+ handleSetActiveTab('communities')} />
)}
{(isKindSpecificPage || showGlobalFeed) && (
- handleSetActiveTab('global')} />
+ handleSetActiveTab('global')} />
)}
{showSavedFeedTabs && savedFeeds.map((feed) => (
-
))}
{showSavedFeedTabs && hashtags.map((tag) => (
- void;
- disabled?: boolean;
- className?: string;
-}
-
-/** Shared tab button used across feed pages (Follows / Global / etc.). */
-export function FeedTabButton({ label, active, onClick, disabled, className }: FeedTabButtonProps) {
- return (
-
- );
-}
diff --git a/src/components/TabButton.tsx b/src/components/TabButton.tsx
new file mode 100644
index 00000000..b9705f43
--- /dev/null
+++ b/src/components/TabButton.tsx
@@ -0,0 +1,56 @@
+import { cn } from '@/lib/utils';
+
+interface TabButtonProps {
+ /** Tab display label. */
+ label: string;
+ /** Whether this tab is currently selected. */
+ active: boolean;
+ /** Called when the tab is clicked. Scroll-to-top is handled internally. */
+ onClick: () => void;
+ /** Disable the button (e.g. when logged out). */
+ disabled?: boolean;
+ /** Extra classes forwarded to the `
);
}
-
-// ---------------------------------------------------------------------------
-// Tab Button
-// ---------------------------------------------------------------------------
-
-
diff --git a/src/pages/EventsFeedPage.tsx b/src/pages/EventsFeedPage.tsx
index 63763704..27d17945 100644
--- a/src/pages/EventsFeedPage.tsx
+++ b/src/pages/EventsFeedPage.tsx
@@ -11,12 +11,12 @@ import { Skeleton } from '@/components/ui/skeleton';
import { PullToRefresh } from '@/components/PullToRefresh';
import { KindInfoButton } from '@/components/KindInfoButton';
import { NoteCard } from '@/components/NoteCard';
+import { TabButton } from '@/components/TabButton';
import { useFeed } from '@/hooks/useFeed';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useFeedTab } from '@/hooks/useFeedTab';
import { useAppContext } from '@/hooks/useAppContext';
import { useLayoutOptions } from '@/contexts/LayoutContext';
-import { FeedTabButton } from '@/components/FeedTabButton';
import { useMuteList } from '@/hooks/useMuteList';
import { isEventMuted } from '@/lib/muteHelpers';
import { getExtraKindDef } from '@/lib/extraKinds';
@@ -120,8 +120,8 @@ export function EventsFeedPage() {
{/* Follows / Global tabs */}
{user && (
- setActiveTab('follows')} />
- setActiveTab('global')} />
+ setActiveTab('follows')} />
+ setActiveTab('global')} />
)}
@@ -183,5 +183,3 @@ function EventCardSkeleton() {
);
}
-
-
diff --git a/src/pages/ListDetailPage.tsx b/src/pages/ListDetailPage.tsx
index 449a6c3f..f0e80402 100644
--- a/src/pages/ListDetailPage.tsx
+++ b/src/pages/ListDetailPage.tsx
@@ -39,6 +39,7 @@ import { shareOrCopy } from '@/lib/share';
import { getRepostKind } from '@/lib/feedUtils';
import { DITTO_RELAY } from '@/lib/appRelays';
import { toast } from '@/hooks/useToast';
+import { TabButton } from '@/components/TabButton';
import { cn, STICKY_HEADER_CLASS } from '@/lib/utils';
import type { NostrEvent } from '@nostrify/nostrify';
import type { UserList } from '@/hooks/useUserLists';
@@ -524,45 +525,33 @@ export function ListDetailPage() {
)}
-
-
{/* Tab bar */}
-
setActiveTab('feed')}
+ className="py-2.5"
+ indicatorClassName="left-1/4 right-1/4 w-auto h-0.5"
>
Feed
- {activeTab === 'feed' && (
-
- )}
-
-
+ setActiveTab('members')}
+ className="py-2.5"
+ indicatorClassName="left-1/4 right-1/4 w-auto h-0.5"
>
Members
({list.pubkeys.length})
- {activeTab === 'members' && (
-
- )}
-
+
diff --git a/src/pages/NotificationsPage.tsx b/src/pages/NotificationsPage.tsx
index 6ca2cd71..8b556122 100644
--- a/src/pages/NotificationsPage.tsx
+++ b/src/pages/NotificationsPage.tsx
@@ -11,6 +11,7 @@ import { useAppContext } from '@/hooks/useAppContext';
import { Skeleton } from '@/components/ui/skeleton';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { NoteCard } from '@/components/NoteCard';
+import { TabButton } from '@/components/TabButton';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useAuthor } from '@/hooks/useAuthor';
import { useEvent } from '@/hooks/useEvent';
@@ -108,19 +109,14 @@ export function NotificationsPage() {
{/* Tab bar */}
{tabs.map(({ key, label }) => (
-
setActiveTab(key)}
- className={cn(
- 'flex-1 py-3.5 sidebar:py-5 text-sm font-medium sidebar:font-semibold transition-colors relative hover:bg-secondary/40',
- activeTab === key ? 'text-foreground' : 'text-muted-foreground',
- )}
- >
- {label}
- {activeTab === key && (
-
- )}
-
+ className="sidebar:py-5 sidebar:font-semibold"
+ indicatorClassName="sidebar:h-[3px]"
+ />
))}
diff --git a/src/pages/PhotosFeedPage.tsx b/src/pages/PhotosFeedPage.tsx
index fd25b740..4ba6d3e2 100644
--- a/src/pages/PhotosFeedPage.tsx
+++ b/src/pages/PhotosFeedPage.tsx
@@ -23,7 +23,7 @@ import { useMuteList } from '@/hooks/useMuteList';
import { isEventMuted } from '@/lib/muteHelpers';
import { KindInfoButton } from '@/components/KindInfoButton';
import { sidebarItemIcon } from '@/lib/sidebarItems';
-import { FeedTabButton } from '@/components/FeedTabButton';
+import { TabButton } from '@/components/TabButton';
import { getExtraKindDef } from '@/lib/extraKinds';
import type { FeedItem } from '@/lib/feedUtils';
import { MediaCollage, MediaCollageSkeleton, eventToMediaItem } from '@/components/MediaCollage';
@@ -104,8 +104,8 @@ export function PhotosFeedPage() {
{/* Tabs */}
- setActiveTab('follows')} disabled={!user} />
- setActiveTab('global')} />
+ setActiveTab('follows')} disabled={!user} />
+ setActiveTab('global')} />
{/* Grid */}
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
index b9b3d0c2..0e3969a3 100644
--- a/src/pages/ProfilePage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -6,7 +6,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useSeoMeta } from '@unhead/react';
import { nip19 } from 'nostr-tools';
import { Zap, Flame, MoreHorizontal, Share2, ClipboardCopy, ExternalLink, VolumeX, Flag, Bitcoin, Users, Pin, X, QrCode, Check, Copy, Loader2, Download, Palette, Pencil, Trash2, Eye, EyeOff, RefreshCw, MessageSquare, Globe, Mail, Plus, GripVertical, ListPlus, Award } from 'lucide-react';
-import { FeedTabButton } from '@/components/FeedTabButton';
+
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { getAvatarShape, isEmoji, emojiAvatarBorderStyle } from '@/lib/avatarShape';
import { Button } from '@/components/ui/button';
@@ -86,6 +86,7 @@ import { FontPicker } from '@/components/FontPicker';
import { BackgroundPicker } from '@/components/BackgroundPicker';
import { PortalContainerProvider } from '@/contexts/PortalContainerContext';
import { formatNumber } from '@/lib/formatNumber';
+import { TabButton } from '@/components/TabButton';
import { cn, STICKY_HEADER_CLASS } from '@/lib/utils';
import type { AddrCoords } from '@/hooks/useEvent';
import type { FeedItem } from '@/lib/feedUtils';
@@ -364,10 +365,6 @@ function FollowingListModal({ pubkeys, open, onOpenChange, displayName }: Follow
);
}
-// ----- Tab Button -----
-
-
-
type EditableTab = { label: string; isCore: boolean; tab?: ProfileTab };
function SortableTabChip({
@@ -1937,7 +1934,7 @@ export function ProfilePage() {
{!tabEditMode && profileTabsQuery.isFetched && viewTabs.map((tab) => {
const tabId = CORE_TAB_IDS[tab.label] ?? tab.label;
return (
-
- setActiveTab('posts')} className="sidebar:py-5" />
- setActiveTab('accounts')} className="sidebar:py-5" />
+ setActiveTab('posts')} className="sidebar:py-5" />
+ setActiveTab('accounts')} className="sidebar:py-5" />
@@ -816,8 +814,6 @@ export function SearchPage() {
/* ── Shared sub-components ── */
-
-
function AccountItem({ profile, isFollowed }: { profile: { pubkey: string; metadata: Record; event?: { tags: string[][] } }; isFollowed: boolean }) {
const npub = useMemo(() => nip19.npubEncode(profile.pubkey), [profile.pubkey]);
const metadata = profile.metadata as { name?: string; nip05?: string; picture?: string; about?: string; bot?: boolean };
diff --git a/src/pages/ThemesPage.tsx b/src/pages/ThemesPage.tsx
index 3db4127a..4eb05546 100644
--- a/src/pages/ThemesPage.tsx
+++ b/src/pages/ThemesPage.tsx
@@ -6,7 +6,6 @@ import { Link } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
import type { NostrEvent } from '@nostrify/nostrify';
-import { FeedTabButton } from '@/components/FeedTabButton';
import { NoteCard } from '@/components/NoteCard';
import { PullToRefresh } from '@/components/PullToRefresh';
import { FeedEmptyState } from '@/components/FeedEmptyState';
@@ -14,6 +13,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { ThemeSelector } from '@/components/ThemeSelector';
+import { TabButton } from '@/components/TabButton';
import { useThemeFeed } from '@/hooks/useThemeFeed';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useTheme } from '@/hooks/useTheme';
@@ -115,9 +115,9 @@ export function ThemesPage() {
{/* Tabs */}
- setActiveTab('my-themes')} />
- setActiveTab('follows')} disabled={!user} />
- setActiveTab('global')} />
+ setActiveTab('my-themes')} />
+ setActiveTab('follows')} disabled={!user} />
+ setActiveTab('global')} />
{/* Tab content */}
@@ -186,12 +186,6 @@ export function ThemesPage() {
);
}
-// ---------------------------------------------------------------------------
-// Tab Button
-// ---------------------------------------------------------------------------
-
-
-
// ---------------------------------------------------------------------------
// Skeleton
// ---------------------------------------------------------------------------
diff --git a/src/pages/VideosFeedPage.tsx b/src/pages/VideosFeedPage.tsx
index 375156a3..57c5e211 100644
--- a/src/pages/VideosFeedPage.tsx
+++ b/src/pages/VideosFeedPage.tsx
@@ -45,7 +45,7 @@ import { useVideoThumbnail } from '@/components/VideoPlayer';
import { sidebarItemIcon } from '@/lib/sidebarItems';
import { getExtraKindDef } from '@/lib/extraKinds';
import { timeAgo } from '@/lib/timeAgo';
-import { FeedTabButton } from '@/components/FeedTabButton';
+import { TabButton } from '@/components/TabButton';
import { cn } from '@/lib/utils';
import { getEffectiveStreamStatus } from '@/lib/streamStatus';
import type { FeedItem } from '@/lib/feedUtils';
@@ -790,8 +790,8 @@ export function VideosFeedPage() {
{/* Follows / Global tabs */}
- setFeedTab('follows')} disabled={!user} />
- setFeedTab('global')} />
+ setFeedTab('follows')} disabled={!user} />
+ setFeedTab('global')} />
{/* Live streams strip — follows tab filters by followed authors */}
diff --git a/src/pages/VinesFeedPage.tsx b/src/pages/VinesFeedPage.tsx
index effc0676..f5ba32a9 100644
--- a/src/pages/VinesFeedPage.tsx
+++ b/src/pages/VinesFeedPage.tsx
@@ -45,13 +45,12 @@ import { ProfileHoverCard } from '@/components/ProfileHoverCard';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { getAvatarShape } from '@/lib/avatarShape';
import { Skeleton } from '@/components/ui/skeleton';
-
-
import { CommentsSheet } from '@/components/CommentsSheet';
import { getDisplayName } from '@/lib/getDisplayName';
import { useProfileUrl } from '@/hooks/useProfileUrl';
import { canZap } from '@/lib/canZap';
import { formatNumber } from '@/lib/formatNumber';
+import { TabButton } from '@/components/TabButton';
import { cn } from '@/lib/utils';
const VINE_KIND = 34236;
@@ -788,26 +787,9 @@ function VinesTabBar({ tab, onTabChange, hasUser }: VinesTabBarProps) {
return (
{hasUser && (
- onTabChange('follows')} />
+ onTabChange('follows')} className="sidebar:py-5 sidebar:font-semibold" indicatorClassName="sidebar:h-[3px]" />
)}
- onTabChange('global')} />
+ onTabChange('global')} className="sidebar:py-5 sidebar:font-semibold" indicatorClassName="sidebar:h-[3px]" />
);
}
-
-function VinesTabButton({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) {
- return (
-
- {label}
- {active && (
-
- )}
-
- );
-}