profileShare

rasmusjy / tasteprint

Read-only snapshot

No repository description.

main default branch 169 files Expires Sep 13, 2026, 9:06 AM
Ui.tsx 1,823 bytes
1 import type { ReactNode } from "react";
2 import { AlertCircle, Inbox, LoaderCircle } from "lucide-react";
3
4 export function LoadingScreen({ label = "Loading your Tasteprint" }: { label?: string }) {
5 return (
6 <div className="state-panel" role="status">
7 <LoaderCircle className="spin" size={24} />
8 <p>{label}</p>
9 </div>
10 );
11 }
12
13 export function ErrorState({ message, action }: { message: string; action?: ReactNode }) {
14 return (
15 <div className="state-panel state-error" role="alert">
16 <AlertCircle size={24} />
17 <p>{message}</p>
18 {action}
19 </div>
20 );
21 }
22
23 export function EmptyState({ title, copy, action }: { title: string; copy: string; action?: ReactNode }) {
24 return (
25 <div className="empty-state">
26 <Inbox size={28} aria-hidden="true" />
27 <h3>{title}</h3>
28 <p>{copy}</p>
29 {action}
30 </div>
31 );
32 }
33
34 export function PageHeader({ eyebrow, title, copy, action }: {
35 eyebrow: string;
36 title: string;
37 copy?: string;
38 action?: ReactNode;
39 }) {
40 return (
41 <header className="page-header">
42 <div>
43 <span className="eyebrow">{eyebrow}</span>
44 <h1>{title}</h1>
45 {copy && <p>{copy}</p>}
46 </div>
47 {action && <div className="page-header-action">{action}</div>}
48 </header>
49 );
50 }
51
52 export function StatusPill({ status }: { status: string }) {
53 return <span className={`status-pill status-${status.toLowerCase()}`}>{status.toLowerCase()}</span>;
54 }
55
56 export function Avatar({ name, url, size = "medium" }: { name: string; url?: string | null; size?: "small" | "medium" | "large" }) {
57 const letters = name.split(/\s+/).slice(0, 2).map(part => part[0]?.toUpperCase()).join("");
58 return (
59 <span className={`avatar avatar-${size}`} aria-label={name}>
60 {url ? <img src={url} alt="" /> : letters}
61 </span>
62 );
63 }
64