From a8f2c54dac17590fdb91278c8a08beca254cd24d Mon Sep 17 00:00:00 2001 From: Lemon Date: Wed, 18 Mar 2026 23:41:44 -0700 Subject: [PATCH] Add decorative arc to logo header when no sub-header exists Introduces a hasSubHeader layout option. Pages with their own tab bars set hasSubHeader: true, suppressing the arc on the mobile top bar. Pages without tab bars (Bookmarks, Trends, etc.) get the arc on the logo header automatically, ensuring the bottom-most header always has the curved element. --- src/components/MainLayout.tsx | 4 ++-- src/components/MobileTopBar.tsx | 10 +++++++++- src/contexts/LayoutContext.ts | 9 ++++++++- src/pages/BooksPage.tsx | 3 +++ src/pages/EventsFeedPage.tsx | 2 +- src/pages/Index.tsx | 2 +- src/pages/KindFeedPage.tsx | 2 +- src/pages/NotificationsPage.tsx | 3 +++ src/pages/PhotosFeedPage.tsx | 2 +- src/pages/SearchPage.tsx | 3 +++ src/pages/ThemesPage.tsx | 1 + src/pages/VideosFeedPage.tsx | 2 +- src/pages/VinesFeedPage.tsx | 1 + 13 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/components/MainLayout.tsx b/src/components/MainLayout.tsx index 06b7cc74..09410d1e 100644 --- a/src/components/MainLayout.tsx +++ b/src/components/MainLayout.tsx @@ -64,7 +64,7 @@ function PageSkeleton() { /** Inner component that reads layout options from the context store. */ function MainLayoutInner() { - const { rightSidebar, showFAB = false, fabKind = 1, fabHref, onFabClick, fabIcon, wrapperClassName, noOverscroll, noMaxWidth } = useLayoutSnapshot(); + const { rightSidebar, showFAB = false, fabKind = 1, fabHref, onFabClick, fabIcon, wrapperClassName, noOverscroll, noMaxWidth, hasSubHeader } = useLayoutSnapshot(); const [drawerOpen, setDrawerOpen] = useState(false); const { config } = useAppContext(); @@ -74,7 +74,7 @@ function MainLayoutInner() { {config.magicMouse && } {/* Mobile top bar - only on small screens */} - setDrawerOpen(true)} /> + setDrawerOpen(true)} showArc={!hasSubHeader} /> {/* Mobile drawer */} diff --git a/src/components/MobileTopBar.tsx b/src/components/MobileTopBar.tsx index b31e941f..0ee19362 100644 --- a/src/components/MobileTopBar.tsx +++ b/src/components/MobileTopBar.tsx @@ -5,9 +5,11 @@ import { BarsStaggeredIcon } from '@/components/icons/BarsStaggeredIcon'; interface MobileTopBarProps { onAvatarClick: () => void; + /** Whether to show the decorative arc below the header (when no sub-header exists). */ + showArc?: boolean; } -export function MobileTopBar({ onAvatarClick }: MobileTopBarProps) { +export function MobileTopBar({ onAvatarClick, showArc }: MobileTopBarProps) { const location = useLocation(); const handleLogoClick = useCallback((e: React.MouseEvent) => { @@ -37,6 +39,12 @@ export function MobileTopBar({ onAvatarClick }: MobileTopBarProps) { {/* Right: spacer for symmetry */}
+ {/* Decorative arc — only shown when no sub-header provides its own */} + {showArc && ( + + + + )} ); } diff --git a/src/contexts/LayoutContext.ts b/src/contexts/LayoutContext.ts index 236a67ec..d95936e8 100644 --- a/src/contexts/LayoutContext.ts +++ b/src/contexts/LayoutContext.ts @@ -34,6 +34,12 @@ export interface LayoutOptions { * full-width page layouts (e.g. messaging). */ noMaxWidth?: boolean; + /** + * If true, indicates the page renders its own sub-header with a decorative + * arc (e.g. tab bars). The mobile top bar will skip its own arc to avoid + * doubling up. + */ + hasSubHeader?: boolean; } type Listener = () => void; @@ -102,7 +108,8 @@ export function useLayoutOptions(options: LayoutOptions): void { prev.current.rightSidebar !== options.rightSidebar || prev.current.scrollContainer !== options.scrollContainer || prev.current.noOverscroll !== options.noOverscroll || - prev.current.noMaxWidth !== options.noMaxWidth; + prev.current.noMaxWidth !== options.noMaxWidth || + prev.current.hasSubHeader !== options.hasSubHeader; if (changed) { prev.current = options; diff --git a/src/pages/BooksPage.tsx b/src/pages/BooksPage.tsx index 2e931466..fb86d324 100644 --- a/src/pages/BooksPage.tsx +++ b/src/pages/BooksPage.tsx @@ -18,6 +18,7 @@ import { usePrefetchBookSummaries } from '@/hooks/useBookSummary'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useFeedTab } from '@/hooks/useFeedTab'; import { useAppContext } from '@/hooks/useAppContext'; +import { useLayoutOptions } from '@/contexts/LayoutContext'; import type { ExtraKindDef } from '@/lib/extraKinds'; type FeedTab = 'follows' | 'global'; @@ -45,6 +46,8 @@ export function BooksPage() { description: 'Book reviews, ratings, and discussions from the Nostr community', }); + useLayoutOptions({ hasSubHeader: true }); + const feedQuery = useBookFeed(activeTab); const { diff --git a/src/pages/EventsFeedPage.tsx b/src/pages/EventsFeedPage.tsx index a407dda2..4e7634f4 100644 --- a/src/pages/EventsFeedPage.tsx +++ b/src/pages/EventsFeedPage.tsx @@ -42,7 +42,7 @@ export function EventsFeedPage() { const [activeTab, setActiveTab] = useFeedTab('events', ['follows', 'global']); useSeoMeta({ title: `Events | ${config.appName}` }); - useLayoutOptions({ showFAB: true, fabKind: 31923 }); + useLayoutOptions({ showFAB: true, fabKind: 31923, hasSubHeader: true }); // Calendar events feed const feedQuery = useFeed(activeTab, { kinds: [31922, 31923] }); diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index fdb9c5c7..dd06d45d 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -11,7 +11,7 @@ const Index = () => { description: 'Your content. Your vibe. Your rules.', }); - useLayoutOptions({ showFAB: true, fabKind: 1 }); + useLayoutOptions({ showFAB: true, fabKind: 1, hasSubHeader: true }); return ; }; diff --git a/src/pages/KindFeedPage.tsx b/src/pages/KindFeedPage.tsx index 4543e7c6..99857b65 100644 --- a/src/pages/KindFeedPage.tsx +++ b/src/pages/KindFeedPage.tsx @@ -50,7 +50,7 @@ export function KindFeedPage({ kind, title, icon, emptyMessage, kindDef, backTo }); const fabClick = onFabClick ?? (resolvedDef ? () => setInfoOpen(true) : undefined); - useLayoutOptions({ showFAB, fabKind: primaryKind, fabHref, onFabClick: fabClick }); + useLayoutOptions({ showFAB, fabKind: primaryKind, fabHref, onFabClick: fabClick, hasSubHeader: true }); const kinds = Array.isArray(kind) ? kind : [kind]; diff --git a/src/pages/NotificationsPage.tsx b/src/pages/NotificationsPage.tsx index 8b828950..745a74bc 100644 --- a/src/pages/NotificationsPage.tsx +++ b/src/pages/NotificationsPage.tsx @@ -31,6 +31,7 @@ import { useProfileBadges } from '@/hooks/useProfileBadges'; import { useBadgeDefinitions } from '@/hooks/useBadgeDefinitions'; import { BADGE_DEFINITION_KIND } from '@/lib/badgeUtils'; import { Button } from '@/components/ui/button'; +import { useLayoutOptions } from '@/contexts/LayoutContext'; type NotificationTab = 'all' | 'mentions'; @@ -42,6 +43,8 @@ export function NotificationsPage() { description: 'Your Nostr notifications', }); + useLayoutOptions({ hasSubHeader: true }); + const [activeTab, setActiveTab] = useState('all'); const { user } = useCurrentUser(); const queryClient = useQueryClient(); diff --git a/src/pages/PhotosFeedPage.tsx b/src/pages/PhotosFeedPage.tsx index 94b90ccb..f3784982 100644 --- a/src/pages/PhotosFeedPage.tsx +++ b/src/pages/PhotosFeedPage.tsx @@ -43,7 +43,7 @@ export function PhotosFeedPage() { const [activeTab, setActiveTab] = useFeedTab('photos', ['follows', 'global']); useSeoMeta({ title: `Photos | ${config.appName}`, description: 'Photo posts on Nostr' }); - useLayoutOptions({ showFAB: false }); + useLayoutOptions({ showFAB: false, hasSubHeader: true }); // ── Follows feed (chronological) ── const followsQuery = useFeed('follows', { kinds: [PHOTO_KIND] }); diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index a5fb7b06..36583fe2 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -49,6 +49,7 @@ import { TabButton } from '@/components/TabButton'; // getNostrIdentifierPath removed — identifiers are now handled as autocomplete suggestions import { cn, STICKY_HEADER_CLASS, parseKindFilter } from '@/lib/utils'; import type { TabFilter } from '@/contexts/AppContext'; +import { useLayoutOptions } from '@/contexts/LayoutContext'; import { isRepostKind, parseRepostContent } from '@/lib/feedUtils'; import { nip19 } from 'nostr-tools'; @@ -91,6 +92,8 @@ export function SearchPage() { description: 'Search Nostr', }); + useLayoutOptions({ hasSubHeader: true }); + const [searchParams, setSearchParams] = useSearchParams(); // Derive tab directly from URL — single source of truth diff --git a/src/pages/ThemesPage.tsx b/src/pages/ThemesPage.tsx index 0f91c5b3..b4d4ffde 100644 --- a/src/pages/ThemesPage.tsx +++ b/src/pages/ThemesPage.tsx @@ -47,6 +47,7 @@ export function ThemesPage() { showFAB: activeTab === 'my-themes', onFabClick: handleFabClick, fabIcon: , + hasSubHeader: true, }); // Feed queries for follows/global tabs diff --git a/src/pages/VideosFeedPage.tsx b/src/pages/VideosFeedPage.tsx index 2705f7a6..f82d857b 100644 --- a/src/pages/VideosFeedPage.tsx +++ b/src/pages/VideosFeedPage.tsx @@ -719,7 +719,7 @@ export function VideosFeedPage() { const [feedTab, setFeedTab] = useFeedTab('videos', ['follows', 'global']); useSeoMeta({ title: `Videos | ${config.appName}`, description: 'Videos and live streams on Nostr' }); - useLayoutOptions({ showFAB: false, noOverscroll: true }); + useLayoutOptions({ showFAB: false, noOverscroll: true, hasSubHeader: true }); useEffect(() => { setShowAllVideos(false); }, [feedTab]); // ── Follows: chronological, small page ── diff --git a/src/pages/VinesFeedPage.tsx b/src/pages/VinesFeedPage.tsx index eeaf01e6..b5fbfe8f 100644 --- a/src/pages/VinesFeedPage.tsx +++ b/src/pages/VinesFeedPage.tsx @@ -633,6 +633,7 @@ export function VinesFeedPage() { showFAB: false, scrollContainer, noOverscroll: true, + hasSubHeader: true, }); // Track scroll direction to expand vines when bottom nav hides