File size: 10,065 Bytes
542c765
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
// 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 (
        <div className={cn("min-h-screen text-white", className)}
            style={{ background: colors.bg }}>
            {glow && (
                <div
                    className="fixed inset-0 opacity-[0.03] pointer-events-none"
                    style={{
                        backgroundImage:
                            "radial-gradient(circle at 25% 25%, #FF9933 0%, transparent 50%), radial-gradient(circle at 75% 75%, #22C55E 0%, transparent 50%)",
                    }}
                />
            )}
            <div className={cn("relative max-w-2xl mx-auto px-4 py-6 pb-24", className)}>
                {children}
            </div>
        </div>
    );
}

// ── PageHeader ────────────────────────────────────────────────────────────────
interface PageHeaderProps {
    icon?: string;
    title: string;
    subtitle?: string;
    delay?: number;
}

export function PageHeader({ icon, title, subtitle, delay = 0 }: PageHeaderProps) {
    return (
        <motion.div
            {...motionPresets.fadeUp}
            transition={{ duration: 0.3, delay }}
            className="mb-6"
        >
            <div className="flex items-center gap-3 mb-1">
                {icon && <span className="text-2xl">{icon}</span>}
                <h1 className="text-xl font-bold tracking-tight text-white">{title}</h1>
            </div>
            {subtitle && (
                <p style={{ color: colors.textMuted }} className="text-sm">{subtitle}</p>
            )}
        </motion.div>
    );
}

// ── 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 (
        <motion.div
            {...motionPresets.fadeUp}
            transition={{ duration: 0.3, delay }}
            className={cn("rounded-2xl border overflow-hidden", className)}
            style={{
                background: active ? colors.bgCardHover : colors.bgCard,
                borderColor: accentColor ? `${accentColor}40` : colors.border,
                borderLeft: accentColor ? `3px solid ${accentColor}` : undefined,
                ...props.style,
            }}
            whileHover={{ y: -2, boxShadow: "0 8px 28px rgba(0,0,0,0.3)" }}
            {...props}
        >
            {children}
        </motion.div>
    );
}

// ── SectionLabel ──────────────────────────────────────────────────────────────
export function SectionLabel({ children }: { children: React.ReactNode }) {
    return (
        <p className="text-white/40 text-[10px] font-semibold uppercase tracking-[0.12em] mb-3 flex items-center gap-1.5">
            {children}
        </p>
    );
}

// ── 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 (
        <div
            className="rounded-xl p-3"
            style={{ background: colors.bgSubtle, border: `1px solid ${colors.border}` }}
        >
            <div className="text-lg mb-1">{icon}</div>
            <div className="text-white font-semibold text-sm">
                {value}
                {unit && <span className="text-[10px] ml-0.5" style={{ color: colors.textFaint }}>{unit}</span>}
            </div>
            <div className="text-[10px]" style={{ color: colors.textMuted }}>{label}</div>
        </div>
    );
}

// ── SeverityBadge ─────────────────────────────────────────────────────────────
export function SeverityBadge({ level }: { level: SeverityLevel }) {
    const s = severity[level];
    return (
        <span
            className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-medium whitespace-nowrap"
            style={{ background: s.bg, color: s.color }}
        >
            <span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: s.color }} />
            {s.label}
        </span>
    );
}

// ── 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 (
        <motion.div
            {...motionPresets.fadeUp}
            transition={{ duration: 0.3, delay }}
            className="mb-5 p-3.5 rounded-xl text-xs leading-relaxed flex items-start gap-2.5"
            style={{
                background: `${color}14`,
                border: `1px solid ${color}30`,
                borderLeft: `3px solid ${color}`,
                color,
            }}
        >
            <span className="flex-shrink-0 mt-0.5">⚠️</span>
            <span>{children}</span>
        </motion.div>
    );
}

// ── Button ────────────────────────────────────────────────────────────────────
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
    variant?: "primary" | "ghost" | "success";
    accentColor?: string;
}

export function Button({ variant = "primary", accentColor, className, children, ...props }: ButtonProps) {
    const styles: Record<string, React.CSSProperties> = {
        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 (
        <motion.button
            whileTap={{ scale: 0.97 }}
            className={cn("w-full py-2 rounded-lg text-xs font-medium transition-all cursor-pointer", className)}
            style={styles[variant]}
            {...(props as any)}
        >
            {children}
        </motion.button>
    );
}

// ── LoadingShell ──────────────────────────────────────────────────────────────
// Drop-in loading skeleton that matches the dark theme.
export function LoadingShell({ rows = 4 }: { rows?: number }) {
    return (
        <PageShell>
            <div className="space-y-4">
                <div className="h-8 w-48 rounded-xl animate-pulse" style={{ background: colors.bgSubtle }} />
                <div className="h-48 rounded-2xl animate-pulse" style={{ background: colors.bgSubtle }} />
                <div className="grid grid-cols-2 gap-3">
                    {[...Array(rows)].map((_, i) => (
                        <div key={i} className="h-28 rounded-2xl animate-pulse" style={{ background: colors.bgSubtle }} />
                    ))}
                </div>
            </div>
        </PageShell>
    );
}

// ── 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 (
        <span
            className="text-[10px] px-1.5 py-0.5 rounded-full inline-block"
            style={{ background: `${color}20`, color }}
        >
            {label}
        </span>
    );
}