// components/ui/index.tsx
// ─────────────────────────────────────────────
// All primitives match your NutritionPage aesthetic.
// Use these on every page for instant consistency.
// ─────────────────────────────────────────────
"use client";
import { motion, AnimatePresence, HTMLMotionProps } from "framer-motion";
import { colors, severity, SeverityLevel, motionPresets, sectionLabelClass } from "@/lib/tokens";
import { cn } from "@/lib/utils";
export { sectionLabelClass };
// ── PageShell ─────────────────────────────────────────────────────────────────
// Wraps every page. Provides the dark bg + ambient glow + safe padding.
interface PageShellProps {
children: React.ReactNode;
className?: string;
/** Show the saffron/green ambient glow in the background */
glow?: boolean;
}
export function PageShell({ children, className, glow = true }: PageShellProps) {
return (
);
}
// ── PageHeader ────────────────────────────────────────────────────────────────
interface PageHeaderProps {
icon?: string;
title: string;
subtitle?: string;
delay?: number;
}
export function PageHeader({ icon, title, subtitle, delay = 0 }: PageHeaderProps) {
return (
{icon && {icon}}
{title}
{subtitle && (
{subtitle}
)}
);
}
// ── Card ──────────────────────────────────────────────────────────────────────
interface CardProps extends HTMLMotionProps<"div"> {
children: React.ReactNode;
className?: string;
/** When true, uses the expanded/hover background */
active?: boolean;
/** Coloured left-border accent */
accentColor?: string;
delay?: number;
}
export function Card({ children, className, active, accentColor, delay = 0, ...props }: CardProps) {
return (
{children}
);
}
// ── SectionLabel ──────────────────────────────────────────────────────────────
export function SectionLabel({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
// ── StatGrid + StatCard ───────────────────────────────────────────────────────
// The 3-column target grid from NutritionPage, generalised.
interface StatCardProps {
icon: string;
value: string | number;
unit?: string;
label: string;
}
export function StatCard({ icon, value, unit, label }: StatCardProps) {
return (
{icon}
{value}
{unit && {unit}}
{label}
);
}
// ── SeverityBadge ─────────────────────────────────────────────────────────────
export function SeverityBadge({ level }: { level: SeverityLevel }) {
const s = severity[level];
return (
{s.label}
);
}
// ── Banner ────────────────────────────────────────────────────────────────────
// The orange deficiency/info banner from NutritionPage.
interface BannerProps {
children: React.ReactNode;
color?: string; // defaults to accent orange
delay?: number;
}
export function Banner({ children, color = colors.accent, delay = 0.1 }: BannerProps) {
return (
⚠️
{children}
);
}
// ── Button ────────────────────────────────────────────────────────────────────
interface ButtonProps extends React.ButtonHTMLAttributes {
variant?: "primary" | "ghost" | "success";
accentColor?: string;
}
export function Button({ variant = "primary", accentColor, className, children, ...props }: ButtonProps) {
const styles: Record = {
primary: { background: accentColor ?? colors.accent, color: "#0d0d1a" },
ghost: { background: colors.bgSubtle, color: colors.textSecondary, border: `1px solid ${colors.border}` },
success: { background: colors.okBg, color: colors.ok },
};
return (
{children}
);
}
// ── LoadingShell ──────────────────────────────────────────────────────────────
// Drop-in loading skeleton that matches the dark theme.
export function LoadingShell({ rows = 4 }: { rows?: number }) {
return (
{[...Array(rows)].map((_, i) => (
))}
);
}
// ── Chip ──────────────────────────────────────────────────────────────────────
// Small coloured pill — used for food group tags, report types, etc.
interface ChipProps {
label: string;
color?: string;
}
export function Chip({ label, color = colors.accent }: ChipProps) {
return (
{label}
);
}