From 542592972ac7b00af3860bd713be3747fd7a3462 Mon Sep 17 00:00:00 2001 From: Nils Briggen Date: Mon, 8 Jun 2026 20:57:45 +0200 Subject: [PATCH] UI overhaul "Living Codex" B: primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- src/components/ui/Button.tsx | 10 ++++--- src/components/ui/Codex.tsx | 58 ++++++++++++++++++++++++++++++++++++ src/components/ui/Input.tsx | 4 +-- src/components/ui/Modal.tsx | 2 +- src/components/ui/Page.tsx | 17 +++++++---- 5 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 src/components/ui/Codex.tsx diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx index 95042c3..e7b1bbd 100644 --- a/src/components/ui/Button.tsx +++ b/src/components/ui/Button.tsx @@ -1,7 +1,7 @@ import { forwardRef, type ButtonHTMLAttributes } from 'react'; import { cn } from '@/lib/cn'; -type Variant = 'primary' | 'secondary' | 'ghost' | 'danger'; +type Variant = 'primary' | 'secondary' | 'subtle' | 'ghost' | 'danger'; type Size = 'sm' | 'md' | 'icon'; export interface ButtonProps extends ButtonHTMLAttributes { @@ -10,10 +10,12 @@ export interface ButtonProps extends ButtonHTMLAttributes { } const variants: Record = { - primary: 'bg-accent text-accent-ink hover:opacity-90 font-medium', + // Gilt: a struck-coin gradient with a top highlight + deep edge. + primary: 'bg-gradient-to-b from-accent-soft to-accent text-accent-ink border border-accent-deep font-medium shadow-[inset_0_1px_0_rgba(255,255,255,0.3)] hover:brightness-105 active:brightness-95', secondary: 'bg-elevated text-ink hover:bg-line border border-line', + subtle: 'bg-elevated text-ink border border-transparent hover:border-line', ghost: 'text-muted hover:text-ink hover:bg-elevated', - danger: 'bg-danger text-white hover:opacity-90 font-medium', + danger: 'bg-danger text-white hover:brightness-110 font-medium border border-danger', }; const sizes: Record = { @@ -31,7 +33,7 @@ export const Button = forwardRef(function Button ref={ref} type={type ?? 'button'} className={cn( - 'inline-flex items-center justify-center gap-2 transition-colors', + 'inline-flex items-center justify-center gap-2 transition-[filter,background-color,border-color,box-shadow]', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/60', 'disabled:opacity-50 disabled:pointer-events-none', variants[variant], diff --git a/src/components/ui/Codex.tsx b/src/components/ui/Codex.tsx new file mode 100644 index 0000000..a949c00 --- /dev/null +++ b/src/components/ui/Codex.tsx @@ -0,0 +1,58 @@ +import type { ReactNode } from 'react'; +import { cn } from '@/lib/cn'; + +/** + * Living-Codex display primitives: a thin HP/resource Meter, a tinted Badge, an + * initials Avatar, and an embossed ability-score StatCoin. Token-driven so they + * follow the theme + the --app-accent-hue reskin. + */ + +export function Meter({ value, max, tone, height = 7 }: { value: number; max: number; tone?: string; height?: number }) { + const pct = Math.max(0, Math.min(100, max > 0 ? (value / max) * 100 : 0)); + return ( +
+ +
+ ); +} + +export type BadgeTone = 'default' | 'gold' | 'ember' | 'verdigris' | 'arcane' | 'success'; +const badgeTone: Record = { + default: 'border-line text-muted bg-elevated', + gold: 'border-accent/60 text-accent-deep bg-accent-glow', + ember: 'border-danger/45 text-danger bg-danger-glow', + verdigris: 'border-verdigris/45 text-verdigris', + arcane: 'border-info/45 text-info', + success: 'border-success/45 text-success', +}; +export function Badge({ tone = 'default', children, className }: { tone?: BadgeTone; children: ReactNode; className?: string }) { + return {children}; +} + +export function Avatar({ name, size = 40, tone }: { name: string; size?: number; tone?: string }) { + const initials = name.split(/\s+/).map((w) => w[0]).filter(Boolean).slice(0, 2).join('').toUpperCase() || '?'; + const t = tone ?? 'var(--app-accent)'; + return ( +
+ {initials} +
+ ); +} + +export function StatCoin({ label, value, mod, active }: { label: string; value: number | string; mod: string; active?: boolean }) { + return ( +
+
{label}
+
{value}
+
{mod}
+
+ ); +} diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index e0389d3..9659ab6 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -2,8 +2,8 @@ import { forwardRef, type InputHTMLAttributes, type SelectHTMLAttributes, type T import { cn } from '@/lib/cn'; const base = - 'w-full bg-surface border border-line rounded-md px-3 py-2 text-sm text-ink placeholder:text-muted ' + - 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/60'; + 'w-full bg-surface border border-line rounded-md px-3 py-2 text-sm text-ink placeholder:text-muted transition-[border-color,box-shadow] ' + + 'focus-visible:outline-none focus-visible:border-accent focus-visible:ring-[3px] focus-visible:ring-accent-glow'; export const Input = forwardRef>( function Input({ className, ...props }, ref) { diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index f91721d..ee5281d 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -62,7 +62,7 @@ export function Modal({ open, onClose, title, children, footer, className }: Mod aria-modal="true" aria-label={title} className={cn( - 'relative w-full max-w-lg max-h-[85vh] overflow-auto rounded-lg border border-line bg-panel shadow-2xl', + 'paper-grain relative w-full max-w-lg max-h-[85vh] overflow-auto rounded-xl border border-line bg-panel shadow-2xl', className, )} > diff --git a/src/components/ui/Page.tsx b/src/components/ui/Page.tsx index 19a3122..1d01960 100644 --- a/src/components/ui/Page.tsx +++ b/src/components/ui/Page.tsx @@ -10,19 +10,26 @@ export function Page({ children }: { children: ReactNode }) { 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 ( -
-
-

{title}

- {subtitle &&

{subtitle}

} +
+
+
+ {eyebrow &&
{eyebrow}
} +

{title}

+ {subtitle &&

{subtitle}

} +
+ {actions &&
{actions}
}
- {actions &&
{actions}
} +
); }