feat: enhance user interface with improved styling and responsiveness

- Updated UserAvatar component to accept additional className for better customization.
- Refined ProfilePage layout with responsive avatar styling.
- Enhanced Header component with improved background and text styles.
- Improved PopularTags and TopFriends components with better spacing and text shadows.
- Updated ThoughtCard and ThoughtThread components for better visual hierarchy and responsiveness.
- Enhanced UI components (Button, Badge, Card, DropdownMenu, Input, Popover, Separator, Skeleton, Textarea) with new styles and effects.
- Added a new background image for visual enhancement.
This commit is contained in:
2025-09-07 00:16:51 +02:00
parent c520690f1e
commit f1e891413a
18 changed files with 348 additions and 174 deletions

View File

@@ -41,28 +41,28 @@
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* Frutiger Aero Gradients */
--gradient-fa-blue: 135deg, hsl(217 91% 60%) 0%, hsl(200 90% 70%) 100%;
--gradient-fa-green: 135deg, hsl(155 70% 55%) 0%, hsl(170 80% 65%) 100%;
--gradient-fa-card: 180deg, hsl(var(--card)) 0%, hsl(var(--card)) 90%,
hsl(var(--card)) 100%;
--gradient-fa-gloss: 135deg, rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 100%;
--shadow-fa-sm: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
--shadow-fa-md: 0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06);
--shadow-fa-lg: 0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05);
--fa-inner: inset 0 1px 2px rgba(0, 0, 0, 0.1);
--text-shadow-default: 0 1px 1px rgba(0, 0, 0, 0.2);
--text-shadow-sm: 0 1px 0px rgba(255, 255, 255, 0.4);
--text-shadow-md: 0 2px 2px rgba(0, 0, 0, 0.2);
--text-shadow-lg: 0 4px 4px rgba(0, 0, 0, 0.2);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
@@ -76,27 +76,40 @@
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: hsl(0 0% 98%); /* Light off-white */
--foreground: hsl(222.2 47.4% 11.2%);
--muted: hsl(210 20% 96.1%);
--muted-foreground: hsl(215.4 16.3% 46.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(222.2 47.4% 11.2%);
--card: hsl(0 0% 100%); /* Pure white for a crisp look */
--card-foreground: hsl(222.2 47.4% 11.2%);
--border: hsl(214.3 31.8% 91.4%);
--input: hsl(214.3 31.8% 91.4%);
--ring: hsl(222.2 47.4% 11.2%);
--primary: hsl(217 91% 60%); /* Vibrant Blue */
--primary-foreground: hsl(210 40% 98%);
--secondary: hsl(155 70% 55%); /* Vibrant Green */
--secondary-foreground: hsl(210 40% 98%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(210 40% 98%);
--accent: hsl(210 20% 96.1%);
--accent-foreground: hsl(222.2 47.4% 11.2%);
--radius: 0.75rem; /* Larger border radius */
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
@@ -110,13 +123,143 @@
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--background: hsl(222.2 47.4% 11.2%);
--foreground: hsl(210 40% 98%);
--muted: hsl(217.2 32.4% 14.8%);
--muted-foreground: hsl(215 20.2% 65.1%);
--popover: hsl(222.2 47.4% 11.2%);
--popover-foreground: hsl(210 40% 98%);
--card: hsl(217.2 32.4% 14.8%);
--card-foreground: hsl(210 40% 98%);
--border: hsl(217.2 32.4% 14.8%);
--input: hsl(217.2 32.4% 14.8%);
--ring: hsl(212.7 26.8% 83.9%);
--primary: hsl(217 91% 60%); /* Vibrant Blue (same as light) */
--primary-foreground: hsl(210 40% 98%);
--secondary: hsl(155 70% 55%); /* Vibrant Green (same as light) */
--secondary-foreground: hsl(210 40% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(210 40% 98%);
--accent: hsl(217.2 32.4% 14.8%);
--accent-foreground: hsl(210 40% 98%);
/* Frutiger Aero Gradients for dark mode (slightly adjusted) */
--color-fa-gradient-blue: linear-gradient(
135deg,
hsl(217 91% 45%) 0%,
hsl(200 90% 55%) 100%
);
--color-fa-gradient-green: linear-gradient(
135deg,
hsl(155 70% 40%) 0%,
hsl(170 80% 50%) 100%
);
--color-fa-gradient-card: linear-gradient(
180deg,
hsl(var(--card)) 0%,
hsl(var(--card)) 90%,
hsl(var(--card)) 100%
);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
background: linear-gradient(
135deg,
hsl(var(--background)) 0%,
hsl(var(--background)) 70%,
hsl(var(--primary) / 0.1) 100%
);
}
.glossy-effect::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
border-radius: var(--radius); /* Inherit parent's border radius */
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.4) 0%,
rgba(255, 255, 255, 0.1) 100%
);
opacity: 0.8;
pointer-events: none; /* Allow clicks to pass through */
z-index: 1; /* Ensure it's above the background but below content */
}
.glossy-effect.bottom::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 30%;
border-radius: var(--radius);
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.1) 0%,
rgba(0, 0, 0, 0) 100%
);
pointer-events: none;
z-index: 1;
}
.fa-gradient-blue {
background: linear-gradient(var(--gradient-fa-blue));
}
.fa-gradient-green {
background: linear-gradient(var(--gradient-fa-green));
}
.fa-gradient-card {
background: linear-gradient(var(--gradient-fa-card));
}
.fa-gloss {
position: relative;
}
.fa-gloss::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
border-radius: var(--radius);
background: linear-gradient(var(--gradient-fa-gloss));
opacity: 0.8;
pointer-events: none;
z-index: 1;
}
.fa-gloss.bottom::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 30%;
border-radius: var(--radius);
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.1) 0%,
rgba(0, 0, 0, 0) 100%
);
pointer-events: none;
z-index: 1;
}
}

View File

@@ -96,7 +96,11 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
<div className="flex justify-between items-start">
<div className="flex items-end gap-4">
<div className="w-24 h-24 rounded-full border-4 border-background shrink-0">
<UserAvatar src={user.avatarUrl} alt={user.displayName} />
<UserAvatar
src={user.avatarUrl}
alt={user.displayName}
className="w-full h-full"
/>
</div>
</div>
{/* Action Button */}

View File

@@ -5,22 +5,22 @@ import Link from "next/link";
import { Button } from "./ui/button";
import { UserNav } from "./user-nav";
import { MainNav } from "./main-nav";
import { ThemeToggle } from "./theme-toggle";
export function Header() {
const { token } = useAuth();
return (
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="w-full flex h-14 items-center px-2">
<header className="sticky top-0 z-50 flex justify-center w-full border-b border-primary/20 bg-background/80 backdrop-blur-xl supports-[backdrop-filter]:bg-background/60 glossy-effect bottom">
<div className="container flex h-14 items-center px-2">
<div className="flex gap-2">
<Link href="/" className="flex items-center gap-1">
<span className="hidden font-bold sm:inline-block">Thoughts</span>
<span className="hidden font-bold text-primary sm:inline-block">
Thoughts
</span>
</Link>
<MainNav />
</div>
<div className="flex flex-1 items-center justify-end space-x-2">
<ThemeToggle />
{token ? (
<UserNav />
) : (

View File

@@ -23,22 +23,25 @@ export async function PopularTags() {
}
return (
<Card>
<CardHeader>
<CardTitle>Popular Tags</CardTitle>
<Card className="p-4">
<CardHeader className="p-0 pb-2">
<CardTitle className="text-lg text-shadow-md">Popular Tags</CardTitle>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
<CardContent className="flex flex-wrap gap-2 p-0">
{tags.map((tag) => (
<Link href={`/tags/${tag}`} key={tag}>
<Badge
variant="secondary"
className="hover:bg-accent cursor-pointer"
className="hover:shadow-lg transition-shadow text-shadow-sm cursor-pointer"
>
<Hash className="mr-1 h-3 w-3" />
{tag}
</Badge>
</Link>
))}
{tags.length === 0 && (
<p className="text-sm text-muted-foreground">No popular tags yet.</p>
)}
</CardContent>
</Card>
);

View File

@@ -38,6 +38,7 @@ import {
} from "lucide-react";
import { ReplyForm } from "@/components/reply-form";
import Link from "next/link";
import { cn } from "@/lib/utils";
interface ThoughtCardProps {
thought: Thought;
@@ -83,16 +84,19 @@ export function ThoughtCard({
<>
<div
id={thought.id}
className={!isReply ? "bg-card rounded-xl border shadow-sm" : ""}
className={cn(
"bg-card/70 backdrop-blur-lg shadow-fa-md rounded-xl overflow-hidden glossy-effect bottom",
isReply ? "backdrop-blur-sm shadow-fa-sm p-2" : ""
)}
>
{thought.replyToId && isReply && (
<div className="px-4 pt-2 text-sm text-muted-foreground flex items-center gap-2">
<CornerUpLeft className="h-4 w-4" />
<div className="text-sm text-muted-foreground flex items-center gap-2">
<CornerUpLeft className="h-4 w-4 text-primary/70" />
<span>
Replying to{" "}
<Link
href={`#${thought.replyToId}`}
className="hover:underline text-primary"
className="hover:underline text-primary text-shadow-sm"
>
parent thought
</Link>
@@ -100,16 +104,18 @@ export function ThoughtCard({
</div>
)}
</div>
<Card>
<Card className="mt-2">
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<Link
href={`/users/${author.username}`}
className="flex items-center gap-4"
className="flex items-center gap-4 text-shadow-md"
>
<UserAvatar src={author.avatarUrl} alt={author.username} />
<div className="flex flex-col">
<span className="font-bold">{author.username}</span>
<span className="text-sm text-muted-foreground">{timeAgo}</span>
<span className="text-sm text-muted-foreground text-shadow-sm">
{timeAgo}
</span>
</div>
</Link>
<DropdownMenu>
@@ -138,11 +144,13 @@ export function ThoughtCard({
</DropdownMenu>
</CardHeader>
<CardContent>
<p className="whitespace-pre-wrap break-words">{thought.content}</p>
<p className="whitespace-pre-wrap break-words text-shadow-sm">
{thought.content}
</p>
</CardContent>
{token && (
<CardFooter className="border-t px-4 pt-2 pb-2">
<CardFooter className="border-t px-4 pt-2 pb-2 border-border/50">
<Button
variant="ghost"
size="sm"
@@ -155,7 +163,7 @@ export function ThoughtCard({
)}
{isReplyOpen && (
<div className="border-t p-4">
<div className="border-t p-4 border-border/50 bg-background/50 backdrop-blur-sm">
<ReplyForm
parentThoughtId={thought.id}
onReplySuccess={() => setIsReplyOpen(false)}

View File

@@ -34,7 +34,7 @@ export function ThoughtThread({
/>
{directReplies.length > 0 && (
<div className="pl-6 border-l-2 border-dashed ml-6 flex flex-col gap-4 pt-4">
<div className="pl-6 border-l-2 border-primary/30 border-dashed ml-6 flex flex-col gap-4 pt-4">
{directReplies.map((reply) => (
<ThoughtThread // RECURSIVE CALL
key={reply.id}

View File

@@ -14,12 +14,12 @@ export async function TopFriends({ usernames }: TopFriendsProps) {
if (usernames.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle>Top Friends</CardTitle>
<Card className="p-4">
<CardHeader className="p-0 pb-2">
<CardTitle className="text-lg text-shadow-md">Top Friends</CardTitle>
</CardHeader>
<CardContent>
<p className="text-center text-muted-foreground">
<CardContent className="p-0">
<p className="text-sm text-muted-foreground">
No top friends to display.
</p>
</CardContent>
@@ -40,19 +40,19 @@ export async function TopFriends({ usernames }: TopFriendsProps) {
.map((result) => result.value);
return (
<Card>
<CardHeader>
<CardTitle>Top Friends</CardTitle>
<Card className="p-4">
<CardHeader className="p-0 pb-2">
<CardTitle className="text-lg text-shadow-md">Top Friends</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-4 gap-4">
<CardContent className="p-0">
{friends.map((friend) => (
<Link
href={`/users/${friend.username}`}
key={friend.id}
className="flex flex-col items-center gap-2 text-center group"
className="flex items-center gap-3 py-2 px-2 -mx-2 rounded-lg hover:bg-accent/50 transition-colors"
>
<UserAvatar src={friend.avatarUrl} alt={friend.username} />
<span className="text-xs font-medium truncate w-full group-hover:underline">
<span className="text-xs truncate w-full group-hover:underline font-medium text-shadow-sm">
{friend.displayName || friend.username}
</span>
</Link>

View File

@@ -1,29 +1,28 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80 glossy-effect bottom text-shadow-sm",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80 glossy-effect bottom text-shadow-sm", // Use green for secondary
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80 glossy-effect bottom text-shadow-sm",
outline: "text-foreground glossy-effect bottom text-shadow-sm",
},
},
defaultVariants: {
variant: "default",
},
}
)
);
function Badge({
className,
@@ -32,7 +31,7 @@ function Badge({
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
const Comp = asChild ? Slot : "span";
return (
<Comp
@@ -40,7 +39,7 @@ function Badge({
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
);
}
export { Badge, badgeVariants }
export { Badge, badgeVariants };

View File

@@ -1,31 +1,34 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
// Default button gets blue gradient, gloss, and shadows
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
"fa-gradient-blue text-primary-foreground shadow-fa-md hover:bg-primary/90 active:shadow-fa-inner transition-transform active:scale-[0.98] glossy-effect",
// Secondary gets green gradient
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
"fa-gradient-green text-secondary-foreground shadow-fa-md hover:bg-secondary/90 active:shadow-fa-inner transition-transform active:scale-[0.98] glossy-effect",
// Ghost and Link should be more subtle
ghost: "hover:bg-accent hover:text-accent-foreground rounded-lg", // Keep them simple, maybe a slight blur/gloss on hover
link: "text-primary underline-offset-4 hover:underline",
// Outline button for a transparent-ish, glassy feel
outline:
"border border-input bg-background/80 hover:bg-accent/80 hover:text-accent-foreground backdrop-blur-sm shadow-fa-sm glossy-effect",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
@@ -33,7 +36,7 @@ const buttonVariants = cva(
size: "default",
},
}
)
);
function Button({
className,
@@ -43,9 +46,9 @@ function Button({
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : "button";
return (
<Comp
@@ -53,7 +56,7 @@ function Button({
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
);
}
export { Button, buttonVariants }
export { Button, buttonVariants };

View File

@@ -1,18 +1,18 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
"relative rounded-xl border bg-card/70 backdrop-blur-lg shadow-fa-lg overflow-hidden glossy-effect bottom text-card-foreground flex flex-col gap-6 py-6",
className
)}
{...props}
/>
)
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -25,17 +25,20 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
)}
{...props}
/>
)
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
className={cn(
"leading-none font-semibold tracking-tight text-shadow-md",
className
)}
{...props}
/>
)
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
@@ -45,7 +48,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
@@ -58,7 +61,7 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
)}
{...props}
/>
)
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -68,7 +71,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
className={cn("px-6", className)}
{...props}
/>
)
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -78,7 +81,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
);
}
export {
@@ -89,4 +92,4 @@ export {
CardAction,
CardDescription,
CardContent,
}
};

View File

@@ -1,15 +1,15 @@
"use client"
"use client";
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
@@ -17,7 +17,7 @@ function DropdownMenuPortal({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
);
}
function DropdownMenuTrigger({
@@ -28,7 +28,7 @@ function DropdownMenuTrigger({
data-slot="dropdown-menu-trigger"
{...props}
/>
)
);
}
function DropdownMenuContent({
@@ -42,13 +42,14 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
"bg-popover/80 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1",
"shadow-fa-lg backdrop-blur-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
);
}
function DropdownMenuGroup({
@@ -56,7 +57,7 @@ function DropdownMenuGroup({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
);
}
function DropdownMenuItem({
@@ -65,8 +66,8 @@ function DropdownMenuItem({
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
@@ -79,7 +80,7 @@ function DropdownMenuItem({
)}
{...props}
/>
)
);
}
function DropdownMenuCheckboxItem({
@@ -105,7 +106,7 @@ function DropdownMenuCheckboxItem({
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
);
}
function DropdownMenuRadioGroup({
@@ -116,7 +117,7 @@ function DropdownMenuRadioGroup({
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
);
}
function DropdownMenuRadioItem({
@@ -140,7 +141,7 @@ function DropdownMenuRadioItem({
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
);
}
function DropdownMenuLabel({
@@ -148,7 +149,7 @@ function DropdownMenuLabel({
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
@@ -160,7 +161,7 @@ function DropdownMenuLabel({
)}
{...props}
/>
)
);
}
function DropdownMenuSeparator({
@@ -173,7 +174,7 @@ function DropdownMenuSeparator({
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
);
}
function DropdownMenuShortcut({
@@ -189,13 +190,13 @@ function DropdownMenuShortcut({
)}
{...props}
/>
)
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
@@ -204,7 +205,7 @@ function DropdownMenuSubTrigger({
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
@@ -219,7 +220,7 @@ function DropdownMenuSubTrigger({
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
);
}
function DropdownMenuSubContent({
@@ -235,7 +236,7 @@ function DropdownMenuSubContent({
)}
{...props}
/>
)
);
}
export {
@@ -254,4 +255,4 @@ export {
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}
};

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
@@ -10,12 +10,12 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-fa-inner transition-shadow",
className
)}
{...props}
/>
)
);
}
export { Input }
export { Input };

View File

@@ -1,20 +1,20 @@
"use client"
"use client";
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverContent({
@@ -30,19 +30,20 @@ function PopoverContent({
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
"bg-popover/80 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 outline-hidden",
"shadow-fa-lg backdrop-blur-md",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
);
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

View File

@@ -1,9 +1,9 @@
"use client"
"use client";
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Separator({
className,
@@ -17,12 +17,12 @@ function Separator({
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
"bg-border/50 shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
);
}
export { Separator }
export { Separator };

View File

@@ -1,13 +1,13 @@
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
className={cn("bg-muted/50 animate-pulse rounded-md", className)}
{...props}
/>
)
);
}
export { Skeleton }
export { Skeleton };

View File

@@ -1,6 +1,6 @@
import * as React from "react"
import * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
@@ -8,11 +8,12 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"shadow-fa-inner transition-shadow",
className
)}
{...props}
/>
)
);
}
export { Textarea }
export { Textarea };

View File

@@ -1,15 +1,23 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { cn } from "@/lib/utils";
import { User } from "lucide-react";
interface UserAvatarProps {
src?: string | null;
alt?: string | null;
className?: string;
}
export function UserAvatar({ src, alt }: UserAvatarProps) {
export function UserAvatar({ src, alt, className }: UserAvatarProps) {
return (
<Avatar>
{src && <AvatarImage src={src} alt={alt ?? "User avatar"} />}
<Avatar className={cn("border-2 border-primary/50 shadow-md", className)}>
{src && (
<AvatarImage
className="object-cover object-center"
src={src}
alt={alt ?? "User avatar"}
/>
)}
<AvatarFallback>
<User className="h-5 w-5" />
</AvatarFallback>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB