feat(ui): PageHeader/PageContainer foundation (piloted on Contacts); profile info-first on mobile
- New shared PageHeader + PageContainer (width tokens: narrow/list/detail/wide) to end the max-w drift between sections; piloted on ContactsPage - Profile: the identity/contact sidebar now comes first on mobile (order-first / lg:order-none) instead of being buried under every content section
This commit is contained in:
parent
10ab45383d
commit
4d2c29edb0
3 changed files with 76 additions and 12 deletions
66
src/components/PageHeader.tsx
Normal file
66
src/components/PageHeader.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import type { ComponentType, ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* Shared page header + width tokens (UX foundations).
|
||||
*
|
||||
* Pages previously each hand-rolled their own title markup and container
|
||||
* width (max-w-2xl / -3xl / -4xl / -7xl / unbounded), so navigating between
|
||||
* sections felt like moving between different apps. New/edited pages should
|
||||
* use these; existing pages can be migrated incrementally.
|
||||
*/
|
||||
export function PageHeader({
|
||||
title,
|
||||
subtitle,
|
||||
icon: Icon,
|
||||
accent = '#ccff00',
|
||||
actions,
|
||||
className,
|
||||
}: {
|
||||
title: ReactNode;
|
||||
subtitle?: ReactNode;
|
||||
icon?: ComponentType<{ className?: string }>;
|
||||
/** Title colour. Defaults to the app's lime accent. */
|
||||
accent?: string;
|
||||
actions?: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('mb-5 flex items-start justify-between gap-3', className)}>
|
||||
<div className="min-w-0">
|
||||
<h1
|
||||
className="flex items-center gap-2 text-3xl font-black uppercase leading-none sm:text-4xl"
|
||||
style={{ color: accent, fontFamily: 'Impact, "Arial Black", sans-serif' }}
|
||||
>
|
||||
{Icon && <Icon className="h-7 w-7 shrink-0 sm:h-8 sm:w-8" />}
|
||||
<span className="truncate">{title}</span>
|
||||
</h1>
|
||||
{subtitle && <p className="mt-2 text-sm text-gray-400">{subtitle}</p>}
|
||||
</div>
|
||||
{actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const WIDTHS = {
|
||||
narrow: 'max-w-2xl',
|
||||
list: 'max-w-3xl',
|
||||
detail: 'max-w-4xl',
|
||||
wide: 'max-w-7xl',
|
||||
} as const;
|
||||
|
||||
export function PageContainer({
|
||||
children,
|
||||
width = 'detail',
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
width?: keyof typeof WIDTHS;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('container mx-auto px-4 py-6', WIDTHS[width], className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { useSeoMeta } from '@unhead/react';
|
|||
import { Link } from 'react-router-dom';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { Layout } from '@/components/Layout';
|
||||
import { PageHeader } from '@/components/PageHeader';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
|
@ -221,17 +222,12 @@ export default function ContactsPage() {
|
|||
<Layout>
|
||||
<div className="container max-w-2xl px-4 py-6">
|
||||
{/* Header */}
|
||||
<div className="mb-4">
|
||||
<h1
|
||||
className="flex items-center gap-2 text-3xl font-black uppercase text-teal-400"
|
||||
style={{ fontFamily: 'Impact, "Arial Black", sans-serif' }}
|
||||
>
|
||||
<Users className="h-7 w-7" /> Contacts
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-gray-400">
|
||||
Your scene — crew-mates, follows & conversations
|
||||
</p>
|
||||
</div>
|
||||
<PageHeader
|
||||
title="Contacts"
|
||||
icon={Users}
|
||||
accent="#2dd4bf"
|
||||
subtitle="Your scene — crew-mates, follows & conversations"
|
||||
/>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative mb-4">
|
||||
|
|
|
|||
|
|
@ -706,7 +706,9 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
|||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-4 min-w-0">
|
||||
{/* On mobile the identity/contact cards come first — the info card was
|
||||
previously buried below every content section on phones. */}
|
||||
<div className="space-y-4 min-w-0 order-first lg:order-none">
|
||||
{/* Zap achievements (only renders when this profile has sent zaps) */}
|
||||
<ZapBadgesCard pubkey={pubkey} />
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue