Skip to content

Commit b1e60ef

Browse files
authored
M6 frontend: foundation (tokens, primitives, helpers, components) (#33)
Design tokens in index.css: brand, destructive, event, avatar (5), tier (4), typography (7). shadcn primitives: textarea, tooltip, alert-dialog. Lib helpers: avatar color/initials, relative time (date-fns), greeting, document title. Shared components: Avatar, RingBadge, EventBadge, EmptyState. Reflects design locks from #8-#14 (typography, spacing, color, errors, avatar, time, title) and prepares for M6 Dashboard.
1 parent 283e984 commit b1e60ef

14 files changed

Lines changed: 501 additions & 0 deletions

client/package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@tanstack/react-query-devtools": "^5.100.9",
1919
"class-variance-authority": "^0.7.1",
2020
"clsx": "^2.1.1",
21+
"date-fns": "^4.1.0",
2122
"lucide-react": "^1.14.0",
2223
"radix-ui": "^1.4.3",
2324
"react": "^19.2.5",

client/src/components/Avatar.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { cn } from "@/lib/utils";
2+
import { colorIndex, initials } from "@/lib/avatar";
3+
4+
const VARIANTS = [
5+
"bg-avatar-1-bg text-avatar-1-fg",
6+
"bg-avatar-2-bg text-avatar-2-fg",
7+
"bg-avatar-3-bg text-avatar-3-fg",
8+
"bg-avatar-4-bg text-avatar-4-fg",
9+
"bg-avatar-5-bg text-avatar-5-fg",
10+
] as const;
11+
12+
export function Avatar({
13+
name,
14+
size = "md",
15+
className,
16+
}: {
17+
name: string;
18+
size?: "md" | "lg";
19+
className?: string;
20+
}) {
21+
return (
22+
<div
23+
className={cn(
24+
"inline-flex shrink-0 items-center justify-center rounded-full font-medium",
25+
VARIANTS[colorIndex(name)],
26+
size === "lg" ? "h-12 w-12 text-name" : "h-9 w-9 text-meta",
27+
className,
28+
)}
29+
aria-hidden
30+
>
31+
{initials(name)}
32+
</div>
33+
);
34+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { type LucideIcon } from "lucide-react";
2+
import { cn } from "@/lib/utils";
3+
4+
export function EmptyState({
5+
icon: Icon,
6+
headline,
7+
subtext,
8+
cta,
9+
className,
10+
}: {
11+
icon: LucideIcon;
12+
headline: string;
13+
subtext?: string;
14+
cta?: React.ReactNode;
15+
className?: string;
16+
}) {
17+
return (
18+
<div
19+
className={cn(
20+
"flex flex-col items-center justify-center gap-3 py-12 text-center",
21+
className,
22+
)}
23+
>
24+
<Icon className="h-8 w-8 text-muted-foreground" aria-hidden />
25+
<p className="text-title font-medium">{headline}</p>
26+
{subtext && (
27+
<p className="text-meta text-muted-foreground">{subtext}</p>
28+
)}
29+
{cta && <div className="mt-2">{cta}</div>}
30+
</div>
31+
);
32+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { cn } from "@/lib/utils";
2+
3+
export function EventBadge({
4+
label,
5+
className,
6+
}: {
7+
label: string;
8+
className?: string;
9+
}) {
10+
return (
11+
<span
12+
className={cn(
13+
"inline-block whitespace-nowrap rounded-full bg-event-bg px-2 py-0.5 text-label text-event-fg",
14+
className,
15+
)}
16+
>
17+
{label}
18+
</span>
19+
);
20+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { cn } from "@/lib/utils";
2+
3+
type Ring = "inner_circle" | "network" | "community" | "acquaintances";
4+
5+
const LABELS: Record<Ring, string> = {
6+
inner_circle: "Inner circle",
7+
network: "Network",
8+
community: "Community",
9+
acquaintances: "Acquaintances",
10+
};
11+
12+
const VARIANTS: Record<Ring, string> = {
13+
inner_circle: "bg-tier-inner-bg text-tier-inner-fg",
14+
network: "bg-tier-network-bg text-tier-network-fg",
15+
community: "bg-tier-community-bg text-tier-community-fg",
16+
acquaintances: "bg-tieracquaintances-bg text-tier-acquaintances-fg",
17+
};
18+
19+
export function RingBadge({
20+
ring,
21+
className,
22+
}: {
23+
ring: Ring;
24+
className?: string;
25+
}) {
26+
return (
27+
<span
28+
className={cn(
29+
"inline-block whitespace-nowrap rounded-full px-2 py-0.5 text-label",
30+
VARIANTS[ring],
31+
className,
32+
)}
33+
>
34+
{LABELS[ring]}
35+
</span>
36+
);
37+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
5+
6+
import { cn } from "@/lib/utils"
7+
import { Button } from "@/components/ui/button"
8+
9+
function AlertDialog({
10+
...props
11+
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
12+
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
13+
}
14+
15+
function AlertDialogTrigger({
16+
...props
17+
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
18+
return (
19+
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
20+
)
21+
}
22+
23+
function AlertDialogPortal({
24+
...props
25+
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
26+
return (
27+
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
28+
)
29+
}
30+
31+
function AlertDialogOverlay({
32+
className,
33+
...props
34+
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
35+
return (
36+
<AlertDialogPrimitive.Overlay
37+
data-slot="alert-dialog-overlay"
38+
className={cn(
39+
"fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
40+
className
41+
)}
42+
{...props}
43+
/>
44+
)
45+
}
46+
47+
function AlertDialogContent({
48+
className,
49+
size = "default",
50+
...props
51+
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
52+
size?: "default" | "sm"
53+
}) {
54+
return (
55+
<AlertDialogPortal>
56+
<AlertDialogOverlay />
57+
<AlertDialogPrimitive.Content
58+
data-slot="alert-dialog-content"
59+
data-size={size}
60+
className={cn(
61+
"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
62+
className
63+
)}
64+
{...props}
65+
/>
66+
</AlertDialogPortal>
67+
)
68+
}
69+
70+
function AlertDialogHeader({
71+
className,
72+
...props
73+
}: React.ComponentProps<"div">) {
74+
return (
75+
<div
76+
data-slot="alert-dialog-header"
77+
className={cn(
78+
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
79+
className
80+
)}
81+
{...props}
82+
/>
83+
)
84+
}
85+
86+
function AlertDialogFooter({
87+
className,
88+
...props
89+
}: React.ComponentProps<"div">) {
90+
return (
91+
<div
92+
data-slot="alert-dialog-footer"
93+
className={cn(
94+
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
95+
className
96+
)}
97+
{...props}
98+
/>
99+
)
100+
}
101+
102+
function AlertDialogMedia({
103+
className,
104+
...props
105+
}: React.ComponentProps<"div">) {
106+
return (
107+
<div
108+
data-slot="alert-dialog-media"
109+
className={cn(
110+
"mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
111+
className
112+
)}
113+
{...props}
114+
/>
115+
)
116+
}
117+
118+
function AlertDialogTitle({
119+
className,
120+
...props
121+
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
122+
return (
123+
<AlertDialogPrimitive.Title
124+
data-slot="alert-dialog-title"
125+
className={cn(
126+
"font-heading text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
127+
className
128+
)}
129+
{...props}
130+
/>
131+
)
132+
}
133+
134+
function AlertDialogDescription({
135+
className,
136+
...props
137+
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
138+
return (
139+
<AlertDialogPrimitive.Description
140+
data-slot="alert-dialog-description"
141+
className={cn(
142+
"text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
143+
className
144+
)}
145+
{...props}
146+
/>
147+
)
148+
}
149+
150+
function AlertDialogAction({
151+
className,
152+
variant = "default",
153+
size = "default",
154+
...props
155+
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
156+
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
157+
return (
158+
<Button variant={variant} size={size} asChild>
159+
<AlertDialogPrimitive.Action
160+
data-slot="alert-dialog-action"
161+
className={cn(className)}
162+
{...props}
163+
/>
164+
</Button>
165+
)
166+
}
167+
168+
function AlertDialogCancel({
169+
className,
170+
variant = "outline",
171+
size = "default",
172+
...props
173+
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
174+
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
175+
return (
176+
<Button variant={variant} size={size} asChild>
177+
<AlertDialogPrimitive.Cancel
178+
data-slot="alert-dialog-cancel"
179+
className={cn(className)}
180+
{...props}
181+
/>
182+
</Button>
183+
)
184+
}
185+
186+
export {
187+
AlertDialog,
188+
AlertDialogAction,
189+
AlertDialogCancel,
190+
AlertDialogContent,
191+
AlertDialogDescription,
192+
AlertDialogFooter,
193+
AlertDialogHeader,
194+
AlertDialogMedia,
195+
AlertDialogOverlay,
196+
AlertDialogPortal,
197+
AlertDialogTitle,
198+
AlertDialogTrigger,
199+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as React from "react"
2+
3+
import { cn } from "@/lib/utils"
4+
5+
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6+
return (
7+
<textarea
8+
data-slot="textarea"
9+
className={cn(
10+
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
11+
className
12+
)}
13+
{...props}
14+
/>
15+
)
16+
}
17+
18+
export { Textarea }

0 commit comments

Comments
 (0)