Tighter campaign hero on desktop; align back button to content width; drop boxes

This commit is contained in:
Chad Curtis
2026-05-21 15:09:48 -05:00
parent 7777271df1
commit 421d4f366e
3 changed files with 69 additions and 52 deletions
+7 -3
View File
@@ -111,7 +111,9 @@ export function BeneficiaryDonatePanel({
</div>
</div>
{/* Copyable address */}
{/* Copyable address. Truncated to a single line so it doesn't
wrap into a multi-row block on mobile tap the row to copy
the full address. */}
<div className="space-y-2">
<Label className="text-xs text-muted-foreground uppercase tracking-wide">
Bitcoin address
@@ -119,10 +121,12 @@ export function BeneficiaryDonatePanel({
<button
type="button"
onClick={copyAddress}
className="w-full flex items-center justify-between gap-2 rounded-lg border bg-muted/40 px-3 py-2.5 font-mono text-xs break-all text-left hover:bg-muted/60 motion-safe:transition-colors"
className="w-full flex items-center gap-2 rounded-lg border bg-muted/40 px-3 py-2.5 font-mono text-xs text-left hover:bg-muted/60 motion-safe:transition-colors"
aria-label="Copy Bitcoin address"
>
<span className="break-all">{address}</span>
<span className="flex-1 min-w-0 truncate" title={address}>
{address}
</span>
{copied ? (
<Check className="size-4 text-green-500 shrink-0" />
) : (
+7 -1
View File
@@ -16,7 +16,13 @@ export interface ReplyNode {
/** Renders a fully threaded reply tree with collapsible deep branches. */
export function ThreadedReplyList({ roots }: { roots: ReplyNode[] }) {
return (
<div>
// Drop the trailing border on the last comment in the list — when
// the surrounding page doesn't wrap us in a card, that border
// floats orphaned below the final note. Two selectors are needed
// because the last root may be either a bare <article> (no
// children) or a <div> wrapping an <article> chain. `!important`
// overrides NoteCard's own `border-b border-border` utility.
<div className="[&>article:last-child]:!border-b-transparent [&>div:last-child_article]:!border-b-transparent">
{roots.map((node) => (
<ReplyThread key={node.event.id} node={node} depth={0} />
))}
+55 -48
View File
@@ -431,9 +431,7 @@ function CampaignDetailContent({ campaign }: { campaign: ParsedCampaign }) {
))}
</div>
) : replyTree.length > 0 ? (
<div className="-mx-2 sm:-mx-4 rounded-2xl bg-card border border-border/60 overflow-hidden">
<ThreadedReplyList roots={replyTree} />
</div>
<ThreadedReplyList roots={replyTree} />
) : (
<button
type="button"
@@ -575,7 +573,7 @@ function CampaignHero({
// viewport for an immersive first impression instead of being a
// strip; on larger screens we cap it so the page content below
// stays visible.
<header className="relative isolate w-full overflow-hidden bg-gradient-to-br from-primary/40 via-primary/20 to-secondary min-h-[92svh] sm:min-h-0 sm:aspect-[16/9] lg:aspect-[18/9]">
<header className="relative isolate w-full overflow-hidden bg-gradient-to-br from-primary/40 via-primary/20 to-secondary min-h-[92svh] sm:min-h-0 sm:aspect-[21/9] lg:aspect-[3/1]">
{cover ? (
<img
src={cover}
@@ -603,55 +601,59 @@ function CampaignHero({
className="absolute inset-x-0 top-0 h-24 bg-gradient-to-b from-black/45 to-transparent"
/>
{/* Top controls — back left, admin right. Use chip-style
{/* Top controls — back left, admin right. Contained to the
same max-w-6xl column as the overlay text below so the back
button aligns with the title's left edge. Chip-style
backdrops so they read on any image without an opaque pill. */}
<div className="absolute inset-x-0 top-0 z-10 flex items-center justify-between gap-3 px-4 pt-[max(env(safe-area-inset-top),1rem)] sm:px-6">
<button
onClick={onBack}
className="inline-flex items-center gap-1.5 h-10 pl-2 pr-3.5 rounded-full bg-black/30 text-white backdrop-blur-md hover:bg-black/45 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80 motion-safe:transition-colors"
aria-label="Go back"
>
<ChevronLeft className="size-5" />
<span className="text-sm font-medium hidden sm:inline">Back</span>
</button>
<div className="absolute inset-x-0 top-0 z-10 px-5 sm:px-6 pt-[max(env(safe-area-inset-top),1rem)]">
<div className="max-w-6xl mx-auto flex items-center justify-between gap-3">
<button
onClick={onBack}
className="inline-flex items-center gap-1.5 h-10 pl-2 pr-3.5 rounded-full bg-black/30 text-white backdrop-blur-md hover:bg-black/45 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80 motion-safe:transition-colors"
aria-label="Go back"
>
<ChevronLeft className="size-5" />
<span className="text-sm font-medium hidden sm:inline">Back</span>
</button>
{isCreator && (
<div className="flex items-center gap-1.5">
<Button
asChild
size="sm"
className="h-10 rounded-full bg-black/30 text-white backdrop-blur-md shadow-none hover:bg-black/45 focus-visible:ring-white/80"
>
<Link to={`/campaigns/new?edit=${encodeURIComponent(naddr)}`}>
<Pencil className="size-4 sm:mr-2" />
<span className="hidden sm:inline">Edit</span>
</Link>
</Button>
{campaign.archived ? (
{isCreator && (
<div className="flex items-center gap-1.5">
<Button
type="button"
asChild
size="sm"
onClick={onReopen}
disabled={archiveDisabled}
className="h-10 rounded-full bg-black/30 text-white backdrop-blur-md shadow-none hover:bg-black/45 focus-visible:ring-white/80"
>
<ArchiveRestore className="size-4 sm:mr-2" />
<span className="hidden sm:inline">Reopen</span>
<Link to={`/campaigns/new?edit=${encodeURIComponent(naddr)}`}>
<Pencil className="size-4 sm:mr-2" />
<span className="hidden sm:inline">Edit</span>
</Link>
</Button>
) : (
<Button
type="button"
size="sm"
onClick={onArchive}
disabled={archiveDisabled}
className="h-10 rounded-full bg-black/30 text-white backdrop-blur-md shadow-none hover:bg-black/45 focus-visible:ring-white/80"
>
<Archive className="size-4 sm:mr-2" />
<span className="hidden sm:inline">Archive</span>
</Button>
)}
</div>
)}
{campaign.archived ? (
<Button
type="button"
size="sm"
onClick={onReopen}
disabled={archiveDisabled}
className="h-10 rounded-full bg-black/30 text-white backdrop-blur-md shadow-none hover:bg-black/45 focus-visible:ring-white/80"
>
<ArchiveRestore className="size-4 sm:mr-2" />
<span className="hidden sm:inline">Reopen</span>
</Button>
) : (
<Button
type="button"
size="sm"
onClick={onArchive}
disabled={archiveDisabled}
className="h-10 rounded-full bg-black/30 text-white backdrop-blur-md shadow-none hover:bg-black/45 focus-visible:ring-white/80"
>
<Archive className="size-4 sm:mr-2" />
<span className="hidden sm:inline">Archive</span>
</Button>
)}
</div>
)}
</div>
</div>
{/* Overlay content — sits at the bottom of the image, contained
@@ -816,8 +818,13 @@ function DonateColumn({
: null;
return (
<Card className="overflow-hidden">
<CardContent className="p-5 space-y-5">
// On mobile we drop the Card chrome (no border, no shadow, no
// rounded background) so the donate content flows inline with the
// page instead of being a floating box stacked between the hero
// and the story. On lg+ the sticky right sidebar reinstates the
// card framing so the column reads as a proper aside.
<Card className="overflow-hidden border-0 shadow-none bg-transparent lg:border lg:shadow-sm lg:bg-card">
<CardContent className="p-0 lg:p-5 space-y-5">
{/* Raised stats + progress */}
{statsLoading ? (
<Skeleton className="h-16 w-full" />