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 { Link } from 'react-router-dom';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { Layout } from '@/components/Layout';
|
import { Layout } from '@/components/Layout';
|
||||||
|
import { PageHeader } from '@/components/PageHeader';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
|
@ -221,17 +222,12 @@ export default function ContactsPage() {
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="container max-w-2xl px-4 py-6">
|
<div className="container max-w-2xl px-4 py-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-4">
|
<PageHeader
|
||||||
<h1
|
title="Contacts"
|
||||||
className="flex items-center gap-2 text-3xl font-black uppercase text-teal-400"
|
icon={Users}
|
||||||
style={{ fontFamily: 'Impact, "Arial Black", sans-serif' }}
|
accent="#2dd4bf"
|
||||||
>
|
subtitle="Your scene — crew-mates, follows & conversations"
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="relative mb-4">
|
<div className="relative mb-4">
|
||||||
|
|
|
||||||
|
|
@ -706,7 +706,9 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar */}
|
{/* 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) */}
|
{/* Zap achievements (only renders when this profile has sent zaps) */}
|
||||||
<ZapBadgesCard pubkey={pubkey} />
|
<ZapBadgesCard pubkey={pubkey} />
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue