Files
ttrpg_manager/src/components/ui/Page.tsx
T
NilsBriggen 542592972a UI overhaul "Living Codex" B: primitives
- Button: primary is now a struck-gilt gradient (from-accent-soft to-accent +
  inset highlight + accent-deep edge); add a `subtle` variant.
- Input/Textarea/Select: gold focus (border-accent + 3px ring-accent-glow).
- PageHeader: optional italic eyebrow + larger Spectral title + a gilt hairline rule.
- Modal: rounded-xl + paper-grain panel.
- New display primitives (Codex.tsx): Meter, Badge, Avatar, StatCoin — token-driven,
  ready for the screen ports.

223 unit + 34 e2e green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:57:45 +02:00

67 lines
2.2 KiB
TypeScript

import type { ReactNode } from 'react';
import { Link } from '@tanstack/react-router';
import { useActiveCampaign } from '@/features/campaigns/hooks';
import type { Campaign } from '@/lib/schemas';
export function Page({ children }: { children: ReactNode }) {
return <div className="mx-auto max-w-6xl px-4 py-6">{children}</div>;
}
export function PageHeader({
title,
subtitle,
eyebrow,
actions,
}: {
title: string;
subtitle?: string;
/** small italic accent line above the title (editorial eyebrow) */
eyebrow?: string;
actions?: ReactNode;
}) {
return (
<div className="mb-6">
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
{eyebrow && <div className="font-display text-sm italic text-accent">{eyebrow}</div>}
<h1 className="font-display text-3xl font-semibold tracking-tight text-ink">{title}</h1>
{subtitle && <p className="mt-1 text-sm text-muted">{subtitle}</p>}
</div>
{actions && <div className="flex items-center gap-2">{actions}</div>}
</div>
<hr className="gilt-rule mt-3" />
</div>
);
}
export function EmptyState({ title, hint, action }: { title: string; hint?: string; action?: ReactNode }) {
return (
<div className="rounded-lg border border-dashed border-line bg-panel/50 p-10 text-center">
<p className="font-medium text-ink">{title}</p>
{hint && <p className="mt-1 text-sm text-muted">{hint}</p>}
{action && <div className="mt-4 flex justify-center">{action}</div>}
</div>
);
}
/** Gate a screen on an active campaign; renders a prompt if none is selected. */
export function RequireCampaign({ children }: { children: (campaign: Campaign) => ReactNode }) {
const campaign = useActiveCampaign();
if (!campaign) {
return (
<Page>
<EmptyState
title="No campaign selected"
hint="Pick a campaign from the switcher up top, or create one to get started."
action={
<Link to="/" className="rounded-md bg-accent px-4 py-2 text-sm font-medium text-accent-ink">
Go to Campaigns
</Link>
}
/>
</Page>
);
}
return <>{children(campaign)}</>;
}