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.
This commit is contained in:
@@ -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 && <CursorFireEffect />}
|
||||
|
||||
{/* Mobile top bar - only on small screens */}
|
||||
<MobileTopBar onAvatarClick={() => setDrawerOpen(true)} />
|
||||
<MobileTopBar onAvatarClick={() => setDrawerOpen(true)} showArc={!hasSubHeader} />
|
||||
|
||||
{/* Mobile drawer */}
|
||||
<MobileDrawer open={drawerOpen} onOpenChange={setDrawerOpen} />
|
||||
|
||||
@@ -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 */}
|
||||
<div className="w-7 shrink-0" />
|
||||
</div>
|
||||
{/* Decorative arc — only shown when no sub-header provides its own */}
|
||||
{showArc && (
|
||||
<svg className="absolute left-0 right-0 top-full w-full pointer-events-none" viewBox="0 0 100 12" preserveAspectRatio="none" style={{ height: 20 }}>
|
||||
<path d="M0,0 Q50,12 100,0 Z" className="fill-background/80" />
|
||||
</svg>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function EventsFeedPage() {
|
||||
const [activeTab, setActiveTab] = useFeedTab<FeedTab>('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] });
|
||||
|
||||
+1
-1
@@ -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 <Feed />;
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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<NotificationTab>('all');
|
||||
const { user } = useCurrentUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -43,7 +43,7 @@ export function PhotosFeedPage() {
|
||||
const [activeTab, setActiveTab] = useFeedTab<FeedTab>('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] });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -47,6 +47,7 @@ export function ThemesPage() {
|
||||
showFAB: activeTab === 'my-themes',
|
||||
onFabClick: handleFabClick,
|
||||
fabIcon: <Pencil strokeWidth={3} size={16} />,
|
||||
hasSubHeader: true,
|
||||
});
|
||||
|
||||
// Feed queries for follows/global tabs
|
||||
|
||||
@@ -719,7 +719,7 @@ export function VideosFeedPage() {
|
||||
const [feedTab, setFeedTab] = useFeedTab<FeedTab>('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 ──
|
||||
|
||||
@@ -633,6 +633,7 @@ export function VinesFeedPage() {
|
||||
showFAB: false,
|
||||
scrollContainer,
|
||||
noOverscroll: true,
|
||||
hasSubHeader: true,
|
||||
});
|
||||
|
||||
// Track scroll direction to expand vines when bottom nav hides
|
||||
|
||||
Reference in New Issue
Block a user