refactor: apply consistent formatting to improve code quality
Some checks failed
Deploy to Production / deploy (push) Failing after 2m20s
Lint / lint (push) Successful in 9m37s

Ensure uniform code formatting across components by aligning with the established code style. Adjust imports, indentation, and spacing to enhance readability and maintainability.
This commit is contained in:
Mathis HERRIOT
2026-01-14 17:26:58 +01:00
parent 03e5915fcc
commit 35abd0496e
95 changed files with 6839 additions and 6659 deletions

View File

@@ -18,6 +18,11 @@
"enabled": true, "enabled": true,
"rules": { "rules": {
"recommended": true, "recommended": true,
"a11y": {
"useAriaPropsForRole": "warn",
"useSemanticElements": "warn",
"useFocusableInteractive": "warn"
},
"suspicious": { "suspicious": {
"noUnknownAtRules": "off" "noUnknownAtRules": "off"
} }

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import * as React from "react";
import Link from "next/link";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";
import * as React from "react";
import { useForm } from "react-hook-form";
import * as z from "zod"; import * as z from "zod";
import { useAuth } from "@/providers/auth-provider";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -15,8 +15,6 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { import {
Form, Form,
FormControl, FormControl,
@@ -25,11 +23,14 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { ArrowLeft } from "lucide-react"; import { Input } from "@/components/ui/input";
import { useAuth } from "@/providers/auth-provider";
const loginSchema = z.object({ const loginSchema = z.object({
email: z.string().email({ message: "Email invalide" }), email: z.string().email({ message: "Email invalide" }),
password: z.string().min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }), password: z
.string()
.min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }),
}); });
type LoginFormValues = z.infer<typeof loginSchema>; type LoginFormValues = z.infer<typeof loginSchema>;
@@ -50,7 +51,7 @@ export default function LoginPage() {
setLoading(true); setLoading(true);
try { try {
await login(values.email, values.password); await login(values.email, values.password);
} catch (error) { } catch (_error) {
// Error is handled in useAuth via toast // Error is handled in useAuth via toast
} finally { } finally {
setLoading(false); setLoading(false);
@@ -112,7 +113,10 @@ export default function LoginPage() {
<CardFooter className="flex flex-col space-y-2"> <CardFooter className="flex flex-col space-y-2">
<p className="text-sm text-center text-muted-foreground"> <p className="text-sm text-center text-muted-foreground">
Vous n'avez pas de compte ?{" "} Vous n'avez pas de compte ?{" "}
<Link href="/register" className="text-primary hover:underline font-medium"> <Link
href="/register"
className="text-primary hover:underline font-medium"
>
S'inscrire S'inscrire
</Link> </Link>
</p> </p>

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import * as React from "react";
import Link from "next/link";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";
import * as React from "react";
import { useForm } from "react-hook-form";
import * as z from "zod"; import * as z from "zod";
import { useAuth } from "@/providers/auth-provider";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -15,7 +15,6 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { import {
Form, Form,
FormControl, FormControl,
@@ -24,12 +23,17 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { ArrowLeft } from "lucide-react"; import { Input } from "@/components/ui/input";
import { useAuth } from "@/providers/auth-provider";
const registerSchema = z.object({ const registerSchema = z.object({
username: z.string().min(3, { message: "Le pseudo doit faire au moins 3 caractères" }), username: z
.string()
.min(3, { message: "Le pseudo doit faire au moins 3 caractères" }),
email: z.string().email({ message: "Email invalide" }), email: z.string().email({ message: "Email invalide" }),
password: z.string().min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }), password: z
.string()
.min(6, { message: "Le mot de passe doit faire au moins 6 caractères" }),
displayName: z.string().optional(), displayName: z.string().optional(),
}); });
@@ -53,7 +57,7 @@ export default function RegisterPage() {
setLoading(true); setLoading(true);
try { try {
await register(values); await register(values);
} catch (error) { } catch (_error) {
// Error handled in useAuth // Error handled in useAuth
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -1,14 +1,23 @@
"use client"; "use client";
import * as React from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import * as React from "react";
import { ContentService } from "@/services/content.service";
import { ContentCard } from "@/components/content-card"; import { ContentCard } from "@/components/content-card";
import type { Content } from "@/types/content"; import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from "@/components/ui/dialog";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { ContentService } from "@/services/content.service";
import type { Content } from "@/types/content";
export default function MemeModal({ params }: { params: Promise<{ slug: string }> }) { export default function MemeModal({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = React.use(params); const { slug } = React.use(params);
const router = useRouter(); const router = useRouter();
const [content, setContent] = React.useState<Content | null>(null); const [content, setContent] = React.useState<Content | null>(null);
@@ -24,8 +33,12 @@ export default function MemeModal({ params }: { params: Promise<{ slug: string }
return ( return (
<Dialog open onOpenChange={(open) => !open && router.back()}> <Dialog open onOpenChange={(open) => !open && router.back()}>
<DialogContent className="max-w-3xl p-0 overflow-hidden bg-transparent border-none"> <DialogContent className="max-w-3xl p-0 overflow-hidden bg-transparent border-none">
<DialogTitle className="sr-only">{content?.title || "Détail du mème"}</DialogTitle> <DialogTitle className="sr-only">
<DialogDescription className="sr-only">Affiche le mème en grand avec ses détails</DialogDescription> {content?.title || "Détail du mème"}
</DialogTitle>
<DialogDescription className="sr-only">
Affiche le mème en grand avec ses détails
</DialogDescription>
{loading ? ( {loading ? (
<div className="h-[500px] flex items-center justify-center bg-zinc-950/50 rounded-lg"> <div className="h-[500px] flex items-center justify-center bg-zinc-950/50 rounded-lg">
<Spinner className="h-10 w-10 text-white" /> <Spinner className="h-10 w-10 text-white" />

View File

@@ -22,7 +22,7 @@ export function useInfiniteScroll({
onLoadMore(); onLoadMore();
} }
}, },
{ threshold } { threshold },
); );
const currentLoader = loaderRef.current; const currentLoader = loaderRef.current;

View File

@@ -1,30 +1,29 @@
import * as React from "react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { CategoryContent } from "@/components/category-content"; import { CategoryContent } from "@/components/category-content";
import { CategoryService } from "@/services/category.service"; import { CategoryService } from "@/services/category.service";
export async function generateMetadata({ export async function generateMetadata({
params params,
}: { }: {
params: Promise<{ slug: string }> params: Promise<{ slug: string }>;
}): Promise<Metadata> { }): Promise<Metadata> {
const { slug } = await params; const { slug } = await params;
try { try {
const categories = await CategoryService.getAll(); const categories = await CategoryService.getAll();
const category = categories.find(c => c.slug === slug); const category = categories.find((c) => c.slug === slug);
return { return {
title: `${category?.name || slug} | MemeGoat`, title: `${category?.name || slug} | MemeGoat`,
description: `Découvrez tous les mèmes de la catégorie ${category?.name || slug} sur MemeGoat.`, description: `Découvrez tous les mèmes de la catégorie ${category?.name || slug} sur MemeGoat.`,
}; };
} catch (error) { } catch (_error) {
return { title: `Catégorie : ${slug} | MemeGoat` }; return { title: `Catégorie : ${slug} | MemeGoat` };
} }
} }
export default async function CategoryPage({ export default async function CategoryPage({
params params,
}: { }: {
params: Promise<{ slug: string }> params: Promise<{ slug: string }>;
}) { }) {
const { slug } = await params; const { slug } = await params;
return <CategoryContent slug={slug} />; return <CategoryContent slug={slug} />;

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import * as React from "react"; import { LayoutGrid } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import * as React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CategoryService } from "@/services/category.service"; import { CategoryService } from "@/services/category.service";
import type { Category } from "@/types/content"; import type { Category } from "@/types/content";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { LayoutGrid } from "lucide-react";
export default function CategoriesPage() { export default function CategoriesPage() {
const [categories, setCategories] = React.useState<Category[]>([]); const [categories, setCategories] = React.useState<Category[]>([]);
@@ -25,15 +25,15 @@ export default function CategoriesPage() {
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
{loading ? ( {loading
Array.from({ length: 6 }).map((_, i) => ( ? Array.from({ length: 6 }).map((_, i) => (
<Card key={i} className="animate-pulse"> /* biome-ignore lint/suspicious/noArrayIndexKey: skeleton items don't have unique IDs */
<Card key={`skeleton-${i}`} className="animate-pulse">
<CardHeader className="h-24 bg-zinc-100 dark:bg-zinc-800 rounded-t-lg" /> <CardHeader className="h-24 bg-zinc-100 dark:bg-zinc-800 rounded-t-lg" />
<CardContent className="h-12" /> <CardContent className="h-12" />
</Card> </Card>
)) ))
) : ( : categories.map((category) => (
categories.map((category) => (
<Link key={category.id} href={`/category/${category.slug}`}> <Link key={category.id} href={`/category/${category.slug}`}>
<Card className="hover:border-primary transition-colors cursor-pointer group h-full"> <Card className="hover:border-primary transition-colors cursor-pointer group h-full">
<CardHeader className="bg-zinc-50 dark:bg-zinc-900 group-hover:bg-primary/5 transition-colors"> <CardHeader className="bg-zinc-50 dark:bg-zinc-900 group-hover:bg-primary/5 transition-colors">
@@ -41,13 +41,13 @@ export default function CategoriesPage() {
</CardHeader> </CardHeader>
<CardContent className="pt-4"> <CardContent className="pt-4">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{category.description || `Découvrez tous les mèmes de la catégorie ${category.name}.`} {category.description ||
`Découvrez tous les mèmes de la catégorie ${category.name}.`}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
</Link> </Link>
)) ))}
)}
</div> </div>
</div> </div>
); );

View File

@@ -1,8 +1,12 @@
import * as React from "react"; import * as React from "react";
import { SidebarProvider, SidebarTrigger, SidebarInset } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/app-sidebar"; import { AppSidebar } from "@/components/app-sidebar";
import { SearchSidebar } from "@/components/search-sidebar";
import { MobileFilters } from "@/components/mobile-filters"; import { MobileFilters } from "@/components/mobile-filters";
import { SearchSidebar } from "@/components/search-sidebar";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
export default function DashboardLayout({ export default function DashboardLayout({
children, children,

View File

@@ -5,7 +5,8 @@ export default function Loading() {
<div className="max-w-2xl mx-auto py-8 px-4 space-y-8"> <div className="max-w-2xl mx-auto py-8 px-4 space-y-8">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{[...Array(3)].map((_, i) => ( {[...Array(3)].map((_, i) => (
<ContentSkeleton key={i} /> /* biome-ignore lint/suspicious/noArrayIndexKey: skeleton items don't have unique IDs */
<ContentSkeleton key={`loading-skeleton-${i}`} />
))} ))}
</div> </div>
</div> </div>

View File

@@ -1,18 +1,17 @@
import * as React from "react";
import type { Metadata } from "next";
import { ContentService } from "@/services/content.service";
import { ContentCard } from "@/components/content-card";
import { Button } from "@/components/ui/button";
import { ChevronLeft } from "lucide-react"; import { ChevronLeft } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { ContentCard } from "@/components/content-card";
import { Button } from "@/components/ui/button";
import { ContentService } from "@/services/content.service";
export const revalidate = 3600; // ISR: Revalider toutes les heures export const revalidate = 3600; // ISR: Revalider toutes les heures
export async function generateMetadata({ export async function generateMetadata({
params params,
}: { }: {
params: Promise<{ slug: string }> params: Promise<{ slug: string }>;
}): Promise<Metadata> { }): Promise<Metadata> {
const { slug } = await params; const { slug } = await params;
try { try {
@@ -24,15 +23,15 @@ export async function generateMetadata({
images: [content.thumbnailUrl || content.url], images: [content.thumbnailUrl || content.url],
}, },
}; };
} catch (error) { } catch (_error) {
return { title: "Mème non trouvé | MemeGoat" }; return { title: "Mème non trouvé | MemeGoat" };
} }
} }
export default async function MemePage({ export default async function MemePage({
params params,
}: { }: {
params: Promise<{ slug: string }> params: Promise<{ slug: string }>;
}) { }) {
const { slug } = await params; const { slug } = await params;
@@ -41,7 +40,10 @@ export default async function MemePage({
return ( return (
<div className="max-w-4xl mx-auto py-8 px-4"> <div className="max-w-4xl mx-auto py-8 px-4">
<Link href="/" className="inline-flex items-center text-sm mb-6 hover:text-primary transition-colors"> <Link
href="/"
className="inline-flex items-center text-sm mb-6 hover:text-primary transition-colors"
>
<ChevronLeft className="h-4 w-4 mr-1" /> <ChevronLeft className="h-4 w-4 mr-1" />
Retour au flux Retour au flux
</Link> </Link>
@@ -57,15 +59,19 @@ export default async function MemePage({
<div className="space-y-4 text-sm"> <div className="space-y-4 text-sm">
<div> <div>
<p className="text-muted-foreground">Publié par</p> <p className="text-muted-foreground">Publié par</p>
<p className="font-medium">{content.author.displayName || content.author.username}</p> <p className="font-medium">
{content.author.displayName || content.author.username}
</p>
</div> </div>
<div> <div>
<p className="text-muted-foreground">Date</p> <p className="text-muted-foreground">Date</p>
<p className="font-medium">{new Date(content.createdAt).toLocaleDateString('fr-FR', { <p className="font-medium">
day: 'numeric', {new Date(content.createdAt).toLocaleDateString("fr-FR", {
month: 'long', day: "numeric",
year: 'numeric' month: "long",
})}</p> year: "numeric",
})}
</p>
</div> </div>
{content.description && ( {content.description && (
<div> <div>
@@ -77,14 +83,16 @@ export default async function MemePage({
</div> </div>
<div className="bg-white dark:bg-zinc-900 p-6 rounded-xl shadow-sm border text-center"> <div className="bg-white dark:bg-zinc-900 p-6 rounded-xl shadow-sm border text-center">
<p className="text-sm text-muted-foreground mb-4">Envie de créer votre propre mème ?</p> <p className="text-sm text-muted-foreground mb-4">
Envie de créer votre propre mème ?
</p>
<Button className="w-full">Utiliser ce template</Button> <Button className="w-full">Utiliser ce template</Button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} catch (error) { } catch (_error) {
notFound(); notFound();
} }
} }

View File

@@ -1,20 +1,23 @@
import * as React from "react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import * as React from "react";
import { HomeContent } from "@/components/home-content"; import { HomeContent } from "@/components/home-content";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "MemeGoat | La meilleure plateforme de mèmes pour les chèvres", title: "MemeGoat | La meilleure plateforme de mèmes pour les chèvres",
description: "Explorez, créez et partagez les meilleurs mèmes de la communauté. Rejoignez le troupeau sur MemeGoat.", description:
"Explorez, créez et partagez les meilleurs mèmes de la communauté. Rejoignez le troupeau sur MemeGoat.",
}; };
export default function HomePage() { export default function HomePage() {
return ( return (
<React.Suspense fallback={ <React.Suspense
fallback={
<div className="flex items-center justify-center p-12"> <div className="flex items-center justify-center p-12">
<Spinner className="h-8 w-8 text-primary" /> <Spinner className="h-8 w-8 text-primary" />
</div> </div>
}> }
>
<HomeContent /> <HomeContent />
</React.Suspense> </React.Suspense>
); );

View File

@@ -1,33 +1,36 @@
"use client"; "use client";
import * as React from "react"; import { Calendar, LogOut, Settings } from "lucide-react";
import { useAuth } from "@/providers/auth-provider";
import { ContentList } from "@/components/content-list";
import { ContentService } from "@/services/content.service";
import { FavoriteService } from "@/services/favorite.service";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Settings, LogOut, Calendar } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import * as React from "react";
import { ContentList } from "@/components/content-list";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useAuth } from "@/providers/auth-provider";
import { ContentService } from "@/services/content.service";
import { FavoriteService } from "@/services/favorite.service";
export default function ProfilePage() { export default function ProfilePage() {
const { user, isAuthenticated, isLoading, logout } = useAuth(); const { user, isAuthenticated, isLoading, logout } = useAuth();
const fetchMyMemes = React.useCallback(
(params: { limit: number; offset: number }) =>
ContentService.getExplore({ ...params, author: user?.username }),
[user?.username],
);
const fetchMyFavorites = React.useCallback(
(params: { limit: number; offset: number }) => FavoriteService.list(params),
[],
);
if (isLoading) return null; if (isLoading) return null;
if (!isAuthenticated || !user) { if (!isAuthenticated || !user) {
redirect("/login"); redirect("/login");
} }
const fetchMyMemes = React.useCallback((params: { limit: number; offset: number }) =>
ContentService.getExplore({ ...params, author: user.username }),
[user.username]);
const fetchMyFavorites = React.useCallback((params: { limit: number; offset: number }) =>
FavoriteService.list(params),
[]);
return ( return (
<div className="max-w-4xl mx-auto py-8 px-4"> <div className="max-w-4xl mx-auto py-8 px-4">
<div className="bg-white dark:bg-zinc-900 rounded-2xl p-8 border shadow-sm mb-8"> <div className="bg-white dark:bg-zinc-900 rounded-2xl p-8 border shadow-sm mb-8">
@@ -40,13 +43,19 @@ export default function ProfilePage() {
</Avatar> </Avatar>
<div className="flex-1 text-center md:text-left space-y-4"> <div className="flex-1 text-center md:text-left space-y-4">
<div> <div>
<h1 className="text-3xl font-bold">{user.displayName || user.username}</h1> <h1 className="text-3xl font-bold">
{user.displayName || user.username}
</h1>
<p className="text-muted-foreground">@{user.username}</p> <p className="text-muted-foreground">@{user.username}</p>
</div> </div>
<div className="flex flex-wrap justify-center md:justify-start gap-4 text-sm text-muted-foreground"> <div className="flex flex-wrap justify-center md:justify-start gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
Membre depuis {new Date(user.createdAt).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })} Membre depuis{" "}
{new Date(user.createdAt).toLocaleDateString("fr-FR", {
month: "long",
year: "numeric",
})}
</span> </span>
</div> </div>
<div className="flex flex-wrap justify-center md:justify-start gap-2"> <div className="flex flex-wrap justify-center md:justify-start gap-2">
@@ -56,7 +65,12 @@ export default function ProfilePage() {
Paramètres Paramètres
</Link> </Link>
</Button> </Button>
<Button variant="ghost" size="sm" onClick={() => logout()} className="text-red-500 hover:text-red-600 hover:bg-red-50"> <Button
variant="ghost"
size="sm"
onClick={() => logout()}
className="text-red-500 hover:text-red-600 hover:bg-red-50"
>
<LogOut className="h-4 w-4 mr-2" /> <LogOut className="h-4 w-4 mr-2" />
Déconnexion Déconnexion
</Button> </Button>

View File

@@ -5,9 +5,11 @@ import { ContentList } from "@/components/content-list";
import { ContentService } from "@/services/content.service"; import { ContentService } from "@/services/content.service";
export default function RecentPage() { export default function RecentPage() {
const fetchFn = React.useCallback((params: { limit: number; offset: number }) => const fetchFn = React.useCallback(
(params: { limit: number; offset: number }) =>
ContentService.getRecent(params.limit, params.offset), ContentService.getRecent(params.limit, params.offset),
[]); [],
);
return <ContentList fetchFn={fetchFn} title="Nouveaux Mèmes" />; return <ContentList fetchFn={fetchFn} title="Nouveaux Mèmes" />;
} }

View File

@@ -5,9 +5,11 @@ import { ContentList } from "@/components/content-list";
import { ContentService } from "@/services/content.service"; import { ContentService } from "@/services/content.service";
export default function TrendsPage() { export default function TrendsPage() {
const fetchFn = React.useCallback((params: { limit: number; offset: number }) => const fetchFn = React.useCallback(
(params: { limit: number; offset: number }) =>
ContentService.getTrends(params.limit, params.offset), ContentService.getTrends(params.limit, params.offset),
[]); [],
);
return <ContentList fetchFn={fetchFn} title="Top Tendances" />; return <ContentList fetchFn={fetchFn} title="Top Tendances" />;
} }

View File

@@ -1,14 +1,15 @@
"use client"; "use client";
import * as React from "react";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod"; import { Image as ImageIcon, Loader2, Upload, X } from "lucide-react";
import { Upload, Image as ImageIcon, Film, X, Loader2 } from "lucide-react"; import NextImage from "next/image";
import { useRouter } from "next/navigation";
import * as React from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import * as z from "zod";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Form, Form,
FormControl, FormControl,
@@ -26,7 +27,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CategoryService } from "@/services/category.service"; import { CategoryService } from "@/services/category.service";
import { ContentService } from "@/services/content.service"; import { ContentService } from "@/services/content.service";
import type { Category } from "@/types/content"; import type { Category } from "@/types/content";
@@ -90,16 +90,36 @@ export default function UploadPage() {
formData.append("type", values.type); formData.append("type", values.type);
if (values.categoryId) formData.append("categoryId", values.categoryId); if (values.categoryId) formData.append("categoryId", values.categoryId);
if (values.tags) { if (values.tags) {
const tagsArray = values.tags.split(",").map(t => t.trim()).filter(t => t !== ""); const tagsArray = values.tags
tagsArray.forEach(tag => formData.append("tags[]", tag)); .split(",")
.map((t) => t.trim())
.filter((t) => t !== "");
for (const tag of tagsArray) {
formData.append("tags[]", tag);
}
} }
await ContentService.upload(formData); await ContentService.upload(formData);
toast.success("Mème uploadé avec succès !"); toast.success("Mème uploadé avec succès !");
router.push("/"); router.push("/");
} catch (error: any) { } catch (error: unknown) {
console.error("Upload failed:", error); console.error("Upload failed:", error);
toast.error(error.response?.data?.message || "Échec de l'upload. Êtes-vous connecté ?"); let errorMessage = "Échec de l'upload. Êtes-vous connecté ?";
if (
error &&
typeof error === "object" &&
"response" in error &&
error.response &&
typeof error.response === "object" &&
"data" in error.response &&
error.response.data &&
typeof error.response.data === "object" &&
"message" in error.response.data &&
typeof error.response.data.message === "string"
) {
errorMessage = error.response.data.message;
}
toast.error(errorMessage);
} finally { } finally {
setIsUploading(false); setIsUploading(false);
} }
@@ -120,15 +140,18 @@ export default function UploadPage() {
<div className="space-y-4"> <div className="space-y-4">
<FormLabel>Fichier (Image ou GIF)</FormLabel> <FormLabel>Fichier (Image ou GIF)</FormLabel>
{!preview ? ( {!preview ? (
<div <button
className="border-2 border-dashed rounded-lg p-12 flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-900 cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" type="button"
className="w-full border-2 border-dashed rounded-lg p-12 flex flex-col items-center justify-center bg-zinc-50 dark:bg-zinc-900 cursor-pointer hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
onClick={() => document.getElementById("file-upload")?.click()} onClick={() => document.getElementById("file-upload")?.click()}
> >
<div className="bg-primary/10 p-4 rounded-full mb-4"> <div className="bg-primary/10 p-4 rounded-full mb-4">
<ImageIcon className="h-8 w-8 text-primary" /> <ImageIcon className="h-8 w-8 text-primary" />
</div> </div>
<p className="font-medium">Cliquez pour choisir un fichier</p> <p className="font-medium">Cliquez pour choisir un fichier</p>
<p className="text-xs text-muted-foreground mt-1">PNG, JPG ou GIF jusqu'à 10Mo</p> <p className="text-xs text-muted-foreground mt-1">
PNG, JPG ou GIF jusqu'à 10Mo
</p>
<input <input
id="file-upload" id="file-upload"
type="file" type="file"
@@ -136,19 +159,22 @@ export default function UploadPage() {
accept="image/*,.gif" accept="image/*,.gif"
onChange={handleFileChange} onChange={handleFileChange}
/> />
</div> </button>
) : ( ) : (
<div className="relative rounded-lg overflow-hidden border bg-zinc-100 dark:bg-zinc-800"> <div className="relative rounded-lg overflow-hidden border bg-zinc-100 dark:bg-zinc-800">
<img <div className="relative h-[400px] w-full">
<NextImage
src={preview} src={preview}
alt="Preview" alt="Preview"
className="max-h-[400px] mx-auto object-contain" fill
className="object-contain"
/> />
</div>
<Button <Button
type="button" type="button"
variant="destructive" variant="destructive"
size="icon" size="icon"
className="absolute top-2 right-2 rounded-full" className="absolute top-2 right-2 rounded-full z-10"
onClick={() => { onClick={() => {
setFile(null); setFile(null);
setPreview(null); setPreview(null);
@@ -210,8 +236,10 @@ export default function UploadPage() {
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{categories.map(cat => ( {categories.map((cat) => (
<SelectItem key={cat.id} value={cat.id}>{cat.name}</SelectItem> <SelectItem key={cat.id} value={cat.id}>
{cat.name}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@@ -230,9 +258,7 @@ export default function UploadPage() {
<FormControl> <FormControl>
<Input placeholder="funny, coding, goat..." {...field} /> <Input placeholder="funny, coding, goat..." {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>Séparez les tags par des virgules.</FormDescription>
Séparez les tags par des virgules.
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}

View File

@@ -1,10 +1,11 @@
"use client"; "use client";
import { AlertTriangle, Home, RefreshCw } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react"; import { useEffect } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { AlertTriangle, RefreshCw, Home } from "lucide-react";
import Link from "next/link";
// biome-ignore lint/suspicious/noShadowRestrictedNames: correct use
export default function Error({ export default function Error({
error, error,
reset, reset,
@@ -24,9 +25,12 @@ export default function Error({
<AlertTriangle className="h-12 w-12 text-orange-600 dark:text-orange-400" /> <AlertTriangle className="h-12 w-12 text-orange-600 dark:text-orange-400" />
</div> </div>
</div> </div>
<h1 className="text-4xl font-bold tracking-tight">Oups ! Une erreur est survenue</h1> <h1 className="text-4xl font-bold tracking-tight">
Oups ! Une erreur est survenue
</h1>
<p className="text-muted-foreground text-lg"> <p className="text-muted-foreground text-lg">
La chèvre a glissé sur une peau de banane. Nous essayons de la remettre sur pied. La chèvre a glissé sur une peau de banane. Nous essayons de la remettre sur
pied.
</p> </p>
<div className="flex flex-col sm:flex-row gap-3 justify-center"> <div className="flex flex-col sm:flex-row gap-3 justify-center">
<Button onClick={reset} size="lg" className="gap-2"> <Button onClick={reset} size="lg" className="gap-2">

View File

@@ -6,8 +6,11 @@
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --font-sans:
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--color-sidebar-ring: var(--sidebar-ring); --color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border); --color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -80,34 +83,37 @@
--popover: oklch(0.9911 0 0); --popover: oklch(0.9911 0 0);
--popover-foreground: oklch(0.2435 0 0); --popover-foreground: oklch(0.2435 0 0);
--primary: oklch(0.4341 0.0392 41.9938); --primary: oklch(0.4341 0.0392 41.9938);
--primary-foreground: oklch(1.0000 0 0); --primary-foreground: oklch(1 0 0);
--secondary: oklch(0.9200 0.0651 74.3695); --secondary: oklch(0.92 0.0651 74.3695);
--secondary-foreground: oklch(0.3499 0.0685 40.8288); --secondary-foreground: oklch(0.3499 0.0685 40.8288);
--muted: oklch(0.9521 0 0); --muted: oklch(0.9521 0 0);
--muted-foreground: oklch(0.5032 0 0); --muted-foreground: oklch(0.5032 0 0);
--accent: oklch(0.9310 0 0); --accent: oklch(0.931 0 0);
--accent-foreground: oklch(0.2435 0 0); --accent-foreground: oklch(0.2435 0 0);
--destructive: oklch(0.6271 0.1936 33.3390); --destructive: oklch(0.6271 0.1936 33.339);
--border: oklch(0.8822 0 0); --border: oklch(0.8822 0 0);
--input: oklch(0.8822 0 0); --input: oklch(0.8822 0 0);
--ring: oklch(0.4341 0.0392 41.9938); --ring: oklch(0.4341 0.0392 41.9938);
--chart-1: oklch(0.4341 0.0392 41.9938); --chart-1: oklch(0.4341 0.0392 41.9938);
--chart-2: oklch(0.9200 0.0651 74.3695); --chart-2: oklch(0.92 0.0651 74.3695);
--chart-3: oklch(0.9310 0 0); --chart-3: oklch(0.931 0 0);
--chart-4: oklch(0.9367 0.0523 75.5009); --chart-4: oklch(0.9367 0.0523 75.5009);
--chart-5: oklch(0.4338 0.0437 41.6746); --chart-5: oklch(0.4338 0.0437 41.6746);
--sidebar: oklch(0.9881 0 0); --sidebar: oklch(0.9881 0 0);
--sidebar-foreground: oklch(0.2645 0 0); --sidebar-foreground: oklch(0.2645 0 0);
--sidebar-primary: oklch(0.3250 0 0); --sidebar-primary: oklch(0.325 0 0);
--sidebar-primary-foreground: oklch(0.9881 0 0); --sidebar-primary-foreground: oklch(0.9881 0 0);
--sidebar-accent: oklch(0.9761 0 0); --sidebar-accent: oklch(0.9761 0 0);
--sidebar-accent-foreground: oklch(0.3250 0 0); --sidebar-accent-foreground: oklch(0.325 0 0);
--sidebar-border: oklch(0.9401 0 0); --sidebar-border: oklch(0.9401 0 0);
--sidebar-ring: oklch(0.7731 0 0); --sidebar-ring: oklch(0.7731 0 0);
--destructive-foreground: oklch(1.0000 0 0); --destructive-foreground: oklch(1 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --font-sans:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--shadow-color: oklch(0 0 0); --shadow-color: oklch(0 0 0);
--shadow-opacity: 0.09; --shadow-opacity: 0.09;
--shadow-blur: 5.5px; --shadow-blur: 5.5px;
@@ -118,11 +124,21 @@
--spacing: 0.2rem; --spacing: 0.2rem;
--shadow-2xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04); --shadow-2xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04);
--shadow-xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04); --shadow-xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04);
--shadow-sm: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09); --shadow-sm:
--shadow: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09); 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
--shadow-md: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 2px 4px -0.5px hsl(0 0% 0% / 0.09); 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09);
--shadow-lg: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 4px 6px -0.5px hsl(0 0% 0% / 0.09); --shadow:
--shadow-xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 8px 10px -0.5px hsl(0 0% 0% / 0.09); 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09);
--shadow-md:
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
2.5px 2px 4px -0.5px hsl(0 0% 0% / 0.09);
--shadow-lg:
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
2.5px 4px 6px -0.5px hsl(0 0% 0% / 0.09);
--shadow-xl:
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
2.5px 8px 10px -0.5px hsl(0 0% 0% / 0.09);
--shadow-2xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.22); --shadow-2xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.22);
--tracking-normal: 0em; --tracking-normal: 0em;
} }
@@ -135,35 +151,38 @@
--popover: oklch(0.2134 0 0); --popover: oklch(0.2134 0 0);
--popover-foreground: oklch(0.9491 0 0); --popover-foreground: oklch(0.9491 0 0);
--primary: oklch(0.9247 0.0524 66.1732); --primary: oklch(0.9247 0.0524 66.1732);
--primary-foreground: oklch(0.2490 0.0317 198.7326); --primary-foreground: oklch(0.249 0.0317 198.7326);
--secondary: oklch(0.3163 0.0190 63.6992); --secondary: oklch(0.3163 0.019 63.6992);
--secondary-foreground: oklch(0.9247 0.0524 66.1732); --secondary-foreground: oklch(0.9247 0.0524 66.1732);
--muted: oklch(0.2520 0 0); --muted: oklch(0.252 0 0);
--muted-foreground: oklch(0.7699 0 0); --muted-foreground: oklch(0.7699 0 0);
--accent: oklch(0.2850 0 0); --accent: oklch(0.285 0 0);
--accent-foreground: oklch(0.9491 0 0); --accent-foreground: oklch(0.9491 0 0);
--destructive: oklch(0.6271 0.1936 33.3390); --destructive: oklch(0.6271 0.1936 33.339);
--border: oklch(0.2351 0.0115 91.7467); --border: oklch(0.2351 0.0115 91.7467);
--input: oklch(0.4017 0 0); --input: oklch(0.4017 0 0);
--ring: oklch(0.9247 0.0524 66.1732); --ring: oklch(0.9247 0.0524 66.1732);
--chart-1: oklch(0.9247 0.0524 66.1732); --chart-1: oklch(0.9247 0.0524 66.1732);
--chart-2: oklch(0.3163 0.0190 63.6992); --chart-2: oklch(0.3163 0.019 63.6992);
--chart-3: oklch(0.2850 0 0); --chart-3: oklch(0.285 0 0);
--chart-4: oklch(0.3481 0.0219 67.0001); --chart-4: oklch(0.3481 0.0219 67.0001);
--chart-5: oklch(0.9245 0.0533 67.0855); --chart-5: oklch(0.9245 0.0533 67.0855);
--sidebar: oklch(0.2103 0.0059 285.8852); --sidebar: oklch(0.2103 0.0059 285.8852);
--sidebar-foreground: oklch(0.9674 0.0013 286.3752); --sidebar-foreground: oklch(0.9674 0.0013 286.3752);
--sidebar-primary: oklch(0.4882 0.2172 264.3763); --sidebar-primary: oklch(0.4882 0.2172 264.3763);
--sidebar-primary-foreground: oklch(1.0000 0 0); --sidebar-primary-foreground: oklch(1 0 0);
--sidebar-accent: oklch(0.2739 0.0055 286.0326); --sidebar-accent: oklch(0.2739 0.0055 286.0326);
--sidebar-accent-foreground: oklch(0.9674 0.0013 286.3752); --sidebar-accent-foreground: oklch(0.9674 0.0013 286.3752);
--sidebar-border: oklch(0.2739 0.0055 286.0326); --sidebar-border: oklch(0.2739 0.0055 286.0326);
--sidebar-ring: oklch(0.8711 0.0055 286.2860); --sidebar-ring: oklch(0.8711 0.0055 286.286);
--destructive-foreground: oklch(1.0000 0 0); --destructive-foreground: oklch(1 0 0);
--radius: 0.5rem; --radius: 0.5rem;
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --font-sans:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--shadow-color: oklch(0 0 0); --shadow-color: oklch(0 0 0);
--shadow-opacity: 0.09; --shadow-opacity: 0.09;
--shadow-blur: 5.5px; --shadow-blur: 5.5px;
@@ -174,11 +193,21 @@
--spacing: 0.2rem; --spacing: 0.2rem;
--shadow-2xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04); --shadow-2xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04);
--shadow-xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04); --shadow-xs: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.04);
--shadow-sm: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09); --shadow-sm:
--shadow: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09); 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
--shadow-md: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 2px 4px -0.5px hsl(0 0% 0% / 0.09); 2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09);
--shadow-lg: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 4px 6px -0.5px hsl(0 0% 0% / 0.09); --shadow:
--shadow-xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09), 2.5px 8px 10px -0.5px hsl(0 0% 0% / 0.09); 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
2.5px 1px 2px -0.5px hsl(0 0% 0% / 0.09);
--shadow-md:
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
2.5px 2px 4px -0.5px hsl(0 0% 0% / 0.09);
--shadow-lg:
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
2.5px 4px 6px -0.5px hsl(0 0% 0% / 0.09);
--shadow-xl:
2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.09),
2.5px 8px 10px -0.5px hsl(0 0% 0% / 0.09);
--shadow-2xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.22); --shadow-2xl: 2.5px 3.5px 5.5px 0.5px hsl(0 0% 0% / 0.22);
} }

View File

@@ -1,8 +1,8 @@
"use client"; "use client";
import { AlertCircle, Home } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Home, AlertCircle } from "lucide-react";
export default function NotFound() { export default function NotFound() {
return ( return (
@@ -13,7 +13,9 @@ export default function NotFound() {
<AlertCircle className="h-12 w-12 text-red-600 dark:text-red-400" /> <AlertCircle className="h-12 w-12 text-red-600 dark:text-red-400" />
</div> </div>
</div> </div>
<h1 className="text-4xl font-bold tracking-tight">404 - Perdu dans le troupeau ?</h1> <h1 className="text-4xl font-bold tracking-tight">
404 - Perdu dans le troupeau ?
</h1>
<p className="text-muted-foreground text-lg"> <p className="text-muted-foreground text-lg">
On dirait que ce mème s'est enfui. La chèvre ne l'a pas trouvé. On dirait que ce mème s'est enfui. La chèvre ne l'a pas trouvé.
</p> </p>
@@ -26,9 +28,7 @@ export default function NotFound() {
</Button> </Button>
</div> </div>
</div> </div>
<div className="mt-12 text-8xl grayscale opacity-20 select-none"> <div className="mt-12 text-8xl grayscale opacity-20 select-none">🐐</div>
🐐
</div>
</div> </div>
); );
} }

View File

@@ -1,17 +1,19 @@
import type { MetadataRoute } from "next"; import type { MetadataRoute } from "next";
import { ContentService } from "@/services/content.service";
import { CategoryService } from "@/services/category.service"; import { CategoryService } from "@/services/category.service";
import { ContentService } from "@/services/content.service";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> { export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://memegoat.local"; const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://memegoat.local";
// Pages statiques // Pages statiques
const routes: MetadataRoute.Sitemap = ["", "/trends", "/recent"].map((route) => ({ const routes: MetadataRoute.Sitemap = ["", "/trends", "/recent"].map(
(route) => ({
url: `${baseUrl}${route}`, url: `${baseUrl}${route}`,
lastModified: new Date(), lastModified: new Date(),
changeFrequency: "daily" as const, changeFrequency: "daily" as const,
priority: route === "" ? 1 : 0.8, priority: route === "" ? 1 : 0.8,
})); }),
);
// Catégories // Catégories
try { try {
@@ -23,7 +25,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
priority: 0.6, priority: 0.6,
})); }));
routes.push(...categoryRoutes); routes.push(...categoryRoutes);
} catch (error) { } catch (_error) {
console.error("Sitemap: Failed to fetch categories"); console.error("Sitemap: Failed to fetch categories");
} }
@@ -37,7 +39,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
priority: 0.5, priority: 0.5,
})); }));
routes.push(...memeRoutes); routes.push(...memeRoutes);
} catch (error) { } catch (_error) {
console.error("Sitemap: Failed to fetch memes"); console.error("Sitemap: Failed to fetch memes");
} }

View File

@@ -1,22 +1,35 @@
"use client"; "use client";
import * as React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { import {
Home, ChevronRight,
TrendingUp,
Clock, Clock,
HelpCircle,
Home,
LayoutGrid, LayoutGrid,
LogIn,
LogOut,
PlusCircle, PlusCircle,
Settings, Settings,
HelpCircle, TrendingUp,
ChevronRight,
LogOut,
User as UserIcon, User as UserIcon,
LogIn,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@@ -31,23 +44,9 @@ import {
SidebarMenuSubButton, SidebarMenuSubButton,
SidebarMenuSubItem, SidebarMenuSubItem,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { import { useAuth } from "@/providers/auth-provider";
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { CategoryService } from "@/services/category.service"; import { CategoryService } from "@/services/category.service";
import type { Category } from "@/types/content"; import type { Category } from "@/types/content";
import { useAuth } from "@/providers/auth-provider";
const mainNav = [ const mainNav = [
{ {
@@ -69,7 +68,7 @@ const mainNav = [
export function AppSidebar() { export function AppSidebar() {
const pathname = usePathname(); const pathname = usePathname();
const { user, logout, isAuthenticated, isLoading } = useAuth(); const { user, logout, isAuthenticated } = useAuth();
const [categories, setCategories] = React.useState<Category[]>([]); const [categories, setCategories] = React.useState<Category[]>([]);
React.useEffect(() => { React.useEffect(() => {
@@ -80,9 +79,7 @@ export function AppSidebar() {
<Sidebar collapsible="icon"> <Sidebar collapsible="icon">
<SidebarHeader className="flex items-center justify-center py-4"> <SidebarHeader className="flex items-center justify-center py-4">
<Link href="/" className="flex items-center gap-2 font-bold text-xl"> <Link href="/" className="flex items-center gap-2 font-bold text-xl">
<div className="bg-primary text-primary-foreground p-1 rounded"> <div className="bg-primary text-primary-foreground p-1 rounded">🐐</div>
🐐
</div>
<span className="group-data-[collapsible=icon]:hidden">MemeGoat</span> <span className="group-data-[collapsible=icon]:hidden">MemeGoat</span>
</Link> </Link>
</SidebarHeader> </SidebarHeader>
@@ -122,7 +119,10 @@ export function AppSidebar() {
<SidebarMenuSub> <SidebarMenuSub>
{categories.map((category) => ( {categories.map((category) => (
<SidebarMenuSubItem key={category.id}> <SidebarMenuSubItem key={category.id}>
<SidebarMenuSubButton asChild isActive={pathname === `/category/${category.slug}`}> <SidebarMenuSubButton
asChild
isActive={pathname === `/category/${category.slug}`}
>
<Link href={`/category/${category.slug}`}> <Link href={`/category/${category.slug}`}>
<span>{category.name}</span> <span>{category.name}</span>
</Link> </Link>

View File

@@ -5,9 +5,11 @@ import { ContentList } from "@/components/content-list";
import { ContentService } from "@/services/content.service"; import { ContentService } from "@/services/content.service";
export function CategoryContent({ slug }: { slug: string }) { export function CategoryContent({ slug }: { slug: string }) {
const fetchFn = React.useCallback((p: { limit: number; offset: number }) => const fetchFn = React.useCallback(
(p: { limit: number; offset: number }) =>
ContentService.getExplore({ ...p, category: slug }), ContentService.getExplore({ ...p, category: slug }),
[slug]); [slug],
);
return <ContentList fetchFn={fetchFn} title={`Catégorie : ${slug}`} />; return <ContentList fetchFn={fetchFn} title={`Catégorie : ${slug}`} />;
} }

View File

@@ -1,25 +1,30 @@
"use client"; "use client";
import * as React from "react"; import { Eye, Heart, MoreHorizontal, Share2 } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { Heart, MessageSquare, Share2, MoreHorizontal, Eye } from "lucide-react"; import { useRouter } from "next/navigation";
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; import * as React from "react";
import { Button } from "@/components/ui/button"; import { toast } from "sonner";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import type { Content } from "@/types/content"; import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/components/ui/card";
import { useAuth } from "@/providers/auth-provider"; import { useAuth } from "@/providers/auth-provider";
import { FavoriteService } from "@/services/favorite.service"; import { FavoriteService } from "@/services/favorite.service";
import { useRouter } from "next/navigation"; import type { Content } from "@/types/content";
import { toast } from "sonner";
interface ContentCardProps { interface ContentCardProps {
content: Content; content: Content;
} }
export function ContentCard({ content }: ContentCardProps) { export function ContentCard({ content }: ContentCardProps) {
const { isAuthenticated, user } = useAuth(); const { isAuthenticated } = useAuth();
const router = useRouter(); const router = useRouter();
const [isLiked, setIsLiked] = React.useState(false); const [isLiked, setIsLiked] = React.useState(false);
const [likesCount, setLikesCount] = React.useState(content.favoritesCount); const [likesCount, setLikesCount] = React.useState(content.favoritesCount);
@@ -38,13 +43,13 @@ export function ContentCard({ content }: ContentCardProps) {
if (isLiked) { if (isLiked) {
await FavoriteService.remove(content.id); await FavoriteService.remove(content.id);
setIsLiked(false); setIsLiked(false);
setLikesCount(prev => prev - 1); setLikesCount((prev) => prev - 1);
} else { } else {
await FavoriteService.add(content.id); await FavoriteService.add(content.id);
setIsLiked(true); setIsLiked(true);
setLikesCount(prev => prev + 1); setLikesCount((prev) => prev + 1);
} }
} catch (error) { } catch (_error) {
toast.error("Une erreur est survenue"); toast.error("Une erreur est survenue");
} }
}; };
@@ -57,11 +62,14 @@ export function ContentCard({ content }: ContentCardProps) {
<AvatarFallback>{content.author.username[0].toUpperCase()}</AvatarFallback> <AvatarFallback>{content.author.username[0].toUpperCase()}</AvatarFallback>
</Avatar> </Avatar>
<div className="flex flex-col"> <div className="flex flex-col">
<Link href={`/user/${content.author.username}`} className="text-sm font-semibold hover:underline"> <Link
href={`/user/${content.author.username}`}
className="text-sm font-semibold hover:underline"
>
{content.author.displayName || content.author.username} {content.author.displayName || content.author.username}
</Link> </Link>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{new Date(content.createdAt).toLocaleDateString('fr-FR')} {new Date(content.createdAt).toLocaleDateString("fr-FR")}
</span> </span>
</div> </div>
<Button variant="ghost" size="icon" className="ml-auto h-8 w-8"> <Button variant="ghost" size="icon" className="ml-auto h-8 w-8">
@@ -96,10 +104,10 @@ export function ContentCard({ content }: ContentCardProps) {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className={`gap-1.5 h-8 ${isLiked ? 'text-red-500 hover:text-red-600' : ''}`} className={`gap-1.5 h-8 ${isLiked ? "text-red-500 hover:text-red-600" : ""}`}
onClick={handleLike} onClick={handleLike}
> >
<Heart className={`h-4 w-4 ${isLiked ? 'fill-current' : ''}`} /> <Heart className={`h-4 w-4 ${isLiked ? "fill-current" : ""}`} />
<span className="text-xs">{likesCount}</span> <span className="text-xs">{likesCount}</span>
</Button> </Button>
<Button variant="ghost" size="sm" className="gap-1.5 h-8"> <Button variant="ghost" size="sm" className="gap-1.5 h-8">
@@ -118,9 +126,13 @@ export function ContentCard({ content }: ContentCardProps) {
<div className="w-full space-y-2"> <div className="w-full space-y-2">
<h3 className="font-medium text-sm line-clamp-2">{content.title}</h3> <h3 className="font-medium text-sm line-clamp-2">{content.title}</h3>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{content.tags.slice(0, 3).map((tag, i) => ( {content.tags.slice(0, 3).map((tag, _i) => (
<Badge key={typeof tag === 'string' ? tag : tag.id} variant="secondary" className="text-[10px] py-0 px-1.5"> <Badge
#{typeof tag === 'string' ? tag : tag.name} key={typeof tag === "string" ? tag : tag.id}
variant="secondary"
className="text-[10px] py-0 px-1.5"
>
#{typeof tag === "string" ? tag : tag.name}
</Badge> </Badge>
))} ))}
</div> </div>

View File

@@ -1,15 +1,16 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { ContentCard } from "@/components/content-card";
import { ContentService } from "@/services/content.service";
import type { Content, PaginatedResponse } from "@/types/content";
import { Spinner } from "@/components/ui/spinner";
import { useInfiniteScroll } from "@/app/(dashboard)/_hooks/use-infinite-scroll"; import { useInfiniteScroll } from "@/app/(dashboard)/_hooks/use-infinite-scroll";
import { ContentCard } from "@/components/content-card";
import { Spinner } from "@/components/ui/spinner";
import type { Content, PaginatedResponse } from "@/types/content";
interface ContentListProps { interface ContentListProps {
fetchFn: (params: { limit: number; offset: number }) => Promise<PaginatedResponse<Content>>; fetchFn: (params: {
limit: number;
offset: number;
}) => Promise<PaginatedResponse<Content>>;
title?: string; title?: string;
} }
@@ -29,8 +30,8 @@ export function ContentList({ fetchFn, title }: ContentListProps) {
offset: offset + 10, offset: offset + 10,
}); });
setContents(prev => [...prev, ...response.data]); setContents((prev) => [...prev, ...response.data]);
setOffset(prev => prev + 10); setOffset((prev) => prev + 10);
setHasMore(response.data.length === 10); setHasMore(response.data.length === 10);
} catch (error) { } catch (error) {
console.error("Failed to load more contents:", error); console.error("Failed to load more contents:", error);
@@ -65,7 +66,6 @@ export function ContentList({ fetchFn, title }: ContentListProps) {
fetchInitial(); fetchInitial();
}, [fetchFn]); }, [fetchFn]);
return ( return (
<div className="max-w-2xl mx-auto py-8 px-4 space-y-8"> <div className="max-w-2xl mx-auto py-8 px-4 space-y-8">
{title && <h1 className="text-2xl font-bold">{title}</h1>} {title && <h1 className="text-2xl font-bold">{title}</h1>}
@@ -78,10 +78,14 @@ export function ContentList({ fetchFn, title }: ContentListProps) {
<div ref={loaderRef} className="py-8 flex justify-center"> <div ref={loaderRef} className="py-8 flex justify-center">
{loading && <Spinner className="h-8 w-8 text-primary" />} {loading && <Spinner className="h-8 w-8 text-primary" />}
{!hasMore && contents.length > 0 && ( {!hasMore && contents.length > 0 && (
<p className="text-muted-foreground text-sm italic">Vous avez atteint la fin ! 🐐</p> <p className="text-muted-foreground text-sm italic">
Vous avez atteint la fin ! 🐐
</p>
)} )}
{!loading && contents.length === 0 && ( {!loading && contents.length === 0 && (
<p className="text-muted-foreground text-sm italic">Aucun mème trouvé ici... pour l'instant !</p> <p className="text-muted-foreground text-sm italic">
Aucun mème trouvé ici... pour l'instant !
</p>
)} )}
</div> </div>
</div> </div>

View File

@@ -1,4 +1,9 @@
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
export function ContentSkeleton() { export function ContentSkeleton() {

View File

@@ -1,9 +1,9 @@
"use client"; "use client";
import { useSearchParams } from "next/navigation";
import * as React from "react"; import * as React from "react";
import { ContentList } from "@/components/content-list"; import { ContentList } from "@/components/content-list";
import { ContentService } from "@/services/content.service"; import { ContentService } from "@/services/content.service";
import { useSearchParams } from "next/navigation";
export function HomeContent() { export function HomeContent() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@@ -13,15 +13,17 @@ export function HomeContent() {
const tag = searchParams.get("tag") || undefined; const tag = searchParams.get("tag") || undefined;
const query = searchParams.get("query") || undefined; const query = searchParams.get("query") || undefined;
const fetchFn = React.useCallback((params: { limit: number; offset: number }) => const fetchFn = React.useCallback(
(params: { limit: number; offset: number }) =>
ContentService.getExplore({ ContentService.getExplore({
...params, ...params,
sort, sort,
category, category,
tag, tag,
query query,
}), }),
[sort, category, tag, query]); [sort, category, tag, query],
);
const title = query const title = query
? `Résultats pour "${query}"` ? `Résultats pour "${query}"`

View File

@@ -1,8 +1,13 @@
"use client"; "use client";
import * as React from "react";
import { Filter, Search } from "lucide-react"; import { Filter, Search } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import * as React from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
@@ -10,11 +15,6 @@ import {
SheetTitle, SheetTitle,
SheetTrigger, SheetTrigger,
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { CategoryService } from "@/services/category.service"; import { CategoryService } from "@/services/category.service";
import type { Category } from "@/types/content"; import type { Category } from "@/types/content";
@@ -33,7 +33,8 @@ export function MobileFilters() {
} }
}, [open]); }, [open]);
const updateSearch = React.useCallback((name: string, value: string | null) => { const updateSearch = React.useCallback(
(name: string, value: string | null) => {
const params = new URLSearchParams(searchParams.toString()); const params = new URLSearchParams(searchParams.toString());
if (value) { if (value) {
params.set(name, value); params.set(name, value);
@@ -41,7 +42,9 @@ export function MobileFilters() {
params.delete(name); params.delete(name);
} }
router.push(`${pathname}?${params.toString()}`); router.push(`${pathname}?${params.toString()}`);
}, [router, pathname, searchParams]); },
[router, pathname, searchParams],
);
const handleSearch = (e: React.FormEvent) => { const handleSearch = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -124,16 +127,20 @@ export function MobileFilters() {
<div> <div>
<h3 className="text-sm font-medium mb-3">Tags populaires</h3> <h3 className="text-sm font-medium mb-3">Tags populaires</h3>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{["funny", "coding", "cat", "dog", "work", "relatable", "gaming"].map(tag => ( {["funny", "coding", "cat", "dog", "work", "relatable", "gaming"].map(
(tag) => (
<Badge <Badge
key={tag} key={tag}
variant={searchParams.get("tag") === tag ? "default" : "outline"} variant={searchParams.get("tag") === tag ? "default" : "outline"}
className="cursor-pointer" className="cursor-pointer"
onClick={() => updateSearch("tag", searchParams.get("tag") === tag ? null : tag)} onClick={() =>
updateSearch("tag", searchParams.get("tag") === tag ? null : tag)
}
> >
#{tag} #{tag}
</Badge> </Badge>
))} ),
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,12 +1,12 @@
"use client"; "use client";
import { Filter, Search } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import * as React from "react"; import * as React from "react";
import { Search, Filter } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator"; import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { useRouter, useSearchParams, usePathname } from "next/navigation"; import { Separator } from "@/components/ui/separator";
import { CategoryService } from "@/services/category.service"; import { CategoryService } from "@/services/category.service";
import type { Category } from "@/types/content"; import type { Category } from "@/types/content";
@@ -22,7 +22,8 @@ export function SearchSidebar() {
CategoryService.getAll().then(setCategories).catch(console.error); CategoryService.getAll().then(setCategories).catch(console.error);
}, []); }, []);
const updateSearch = React.useCallback((name: string, value: string | null) => { const updateSearch = React.useCallback(
(name: string, value: string | null) => {
const params = new URLSearchParams(searchParams.toString()); const params = new URLSearchParams(searchParams.toString());
if (value) { if (value) {
params.set(name, value); params.set(name, value);
@@ -32,7 +33,9 @@ export function SearchSidebar() {
// If we are not on explore/trends/recent, maybe we should redirect to home? // If we are not on explore/trends/recent, maybe we should redirect to home?
// For now, let's just update the URL. // For now, let's just update the URL.
router.push(`${pathname}?${params.toString()}`); router.push(`${pathname}?${params.toString()}`);
}, [router, pathname, searchParams]); },
[router, pathname, searchParams],
);
const handleSearch = (e: React.FormEvent) => { const handleSearch = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -113,16 +116,20 @@ export function SearchSidebar() {
<div> <div>
<h3 className="text-sm font-medium mb-3">Tags populaires</h3> <h3 className="text-sm font-medium mb-3">Tags populaires</h3>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{["funny", "coding", "cat", "dog", "work", "relatable", "gaming"].map(tag => ( {["funny", "coding", "cat", "dog", "work", "relatable", "gaming"].map(
(tag) => (
<Badge <Badge
key={tag} key={tag}
variant={searchParams.get("tag") === tag ? "default" : "outline"} variant={searchParams.get("tag") === tag ? "default" : "outline"}
className="cursor-pointer hover:bg-secondary" className="cursor-pointer hover:bg-secondary"
onClick={() => updateSearch("tag", searchParams.get("tag") === tag ? null : tag)} onClick={() =>
updateSearch("tag", searchParams.get("tag") === tag ? null : tag)
}
> >
#{tag} #{tag}
</Badge> </Badge>
))} ),
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,15 +1,15 @@
"use client" "use client";
import * as React from "react" import * as AccordionPrimitive from "@radix-ui/react-accordion";
import * as AccordionPrimitive from "@radix-ui/react-accordion" import { ChevronDownIcon } from "lucide-react";
import { ChevronDownIcon } from "lucide-react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Accordion({ function Accordion({
...props ...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) { }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} /> return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
} }
function AccordionItem({ function AccordionItem({
@@ -22,7 +22,7 @@ function AccordionItem({
className={cn("border-b last:border-b-0", className)} className={cn("border-b last:border-b-0", className)}
{...props} {...props}
/> />
) );
} }
function AccordionTrigger({ function AccordionTrigger({
@@ -36,7 +36,7 @@ function AccordionTrigger({
data-slot="accordion-trigger" data-slot="accordion-trigger"
className={cn( className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180", "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className className,
)} )}
{...props} {...props}
> >
@@ -44,7 +44,7 @@ function AccordionTrigger({
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" /> <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger> </AccordionPrimitive.Trigger>
</AccordionPrimitive.Header> </AccordionPrimitive.Header>
) );
} }
function AccordionContent({ function AccordionContent({
@@ -60,7 +60,7 @@ function AccordionContent({
> >
<div className={cn("pt-0 pb-4", className)}>{children}</div> <div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content> </AccordionPrimitive.Content>
) );
} }
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@@ -1,15 +1,14 @@
"use client" "use client";
import * as React from "react" import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import type * as React from "react";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button"
function AlertDialog({ function AlertDialog({
...props ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} /> return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
} }
function AlertDialogTrigger({ function AlertDialogTrigger({
@@ -17,7 +16,7 @@ function AlertDialogTrigger({
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return ( return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
) );
} }
function AlertDialogPortal({ function AlertDialogPortal({
@@ -25,7 +24,7 @@ function AlertDialogPortal({
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) { }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return ( return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} /> <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
) );
} }
function AlertDialogOverlay({ function AlertDialogOverlay({
@@ -37,11 +36,11 @@ function AlertDialogOverlay({
data-slot="alert-dialog-overlay" data-slot="alert-dialog-overlay"
className={cn( className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function AlertDialogContent({ function AlertDialogContent({
@@ -55,12 +54,12 @@ function AlertDialogContent({
data-slot="alert-dialog-content" data-slot="alert-dialog-content"
className={cn( className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg", "bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className className,
)} )}
{...props} {...props}
/> />
</AlertDialogPortal> </AlertDialogPortal>
) );
} }
function AlertDialogHeader({ function AlertDialogHeader({
@@ -73,7 +72,7 @@ function AlertDialogHeader({
className={cn("flex flex-col gap-2 text-center sm:text-left", className)} className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props} {...props}
/> />
) );
} }
function AlertDialogFooter({ function AlertDialogFooter({
@@ -85,11 +84,11 @@ function AlertDialogFooter({
data-slot="alert-dialog-footer" data-slot="alert-dialog-footer"
className={cn( className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function AlertDialogTitle({ function AlertDialogTitle({
@@ -102,7 +101,7 @@ function AlertDialogTitle({
className={cn("text-lg font-semibold", className)} className={cn("text-lg font-semibold", className)}
{...props} {...props}
/> />
) );
} }
function AlertDialogDescription({ function AlertDialogDescription({
@@ -115,7 +114,7 @@ function AlertDialogDescription({
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) );
} }
function AlertDialogAction({ function AlertDialogAction({
@@ -127,7 +126,7 @@ function AlertDialogAction({
className={cn(buttonVariants(), className)} className={cn(buttonVariants(), className)}
{...props} {...props}
/> />
) );
} }
function AlertDialogCancel({ function AlertDialogCancel({
@@ -139,7 +138,7 @@ function AlertDialogCancel({
className={cn(buttonVariants({ variant: "outline" }), className)} className={cn(buttonVariants({ variant: "outline" }), className)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -154,4 +153,4 @@ export {
AlertDialogDescription, AlertDialogDescription,
AlertDialogAction, AlertDialogAction,
AlertDialogCancel, AlertDialogCancel,
} };

View File

@@ -1,7 +1,7 @@
import * as React from "react" import { cva, type VariantProps } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const alertVariants = cva( const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
@@ -16,8 +16,8 @@ const alertVariants = cva(
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
function Alert({ function Alert({
className, className,
@@ -31,7 +31,7 @@ function Alert({
className={cn(alertVariants({ variant }), className)} className={cn(alertVariants({ variant }), className)}
{...props} {...props}
/> />
) );
} }
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -40,11 +40,11 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
data-slot="alert-title" data-slot="alert-title"
className={cn( className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function AlertDescription({ function AlertDescription({
@@ -56,11 +56,11 @@ function AlertDescription({
data-slot="alert-description" data-slot="alert-description"
className={cn( className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed", "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Alert, AlertTitle, AlertDescription } export { Alert, AlertTitle, AlertDescription };

View File

@@ -1,11 +1,11 @@
"use client" "use client";
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
function AspectRatio({ function AspectRatio({
...props ...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) { }: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} /> return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
} }
export { AspectRatio } export { AspectRatio };

View File

@@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as AvatarPrimitive from "@radix-ui/react-avatar" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Avatar({ function Avatar({
className, className,
@@ -14,11 +14,11 @@ function Avatar({
data-slot="avatar" data-slot="avatar"
className={cn( className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full", "relative flex size-8 shrink-0 overflow-hidden rounded-full",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function AvatarImage({ function AvatarImage({
@@ -31,7 +31,7 @@ function AvatarImage({
className={cn("aspect-square size-full", className)} className={cn("aspect-square size-full", className)}
{...props} {...props}
/> />
) );
} }
function AvatarFallback({ function AvatarFallback({
@@ -43,11 +43,11 @@ function AvatarFallback({
data-slot="avatar-fallback" data-slot="avatar-fallback"
className={cn( className={cn(
"bg-muted flex size-full items-center justify-center rounded-full", "bg-muted flex size-full items-center justify-center rounded-full",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Avatar, AvatarImage, AvatarFallback } export { Avatar, AvatarImage, AvatarFallback };

View File

@@ -1,8 +1,8 @@
import * as React from "react" import { Slot } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full 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 justify-center rounded-full 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",
@@ -22,8 +22,8 @@ const badgeVariants = cva(
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
function Badge({ function Badge({
className, className,
@@ -32,7 +32,7 @@ function Badge({
...props ...props
}: React.ComponentProps<"span"> & }: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) { VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span" const Comp = asChild ? Slot : "span";
return ( return (
<Comp <Comp
@@ -40,7 +40,7 @@ function Badge({
className={cn(badgeVariants({ variant }), className)} className={cn(badgeVariants({ variant }), className)}
{...props} {...props}
/> />
) );
} }
export { Badge, badgeVariants } export { Badge, badgeVariants };

View File

@@ -1,11 +1,11 @@
import * as React from "react" import { Slot } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot" import { ChevronRight, MoreHorizontal } from "lucide-react";
import { ChevronRight, MoreHorizontal } from "lucide-react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} /> return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
} }
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) { function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
@@ -14,11 +14,11 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
data-slot="breadcrumb-list" data-slot="breadcrumb-list"
className={cn( className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5", "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) { function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
@@ -28,7 +28,7 @@ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
className={cn("inline-flex items-center gap-1.5", className)} className={cn("inline-flex items-center gap-1.5", className)}
{...props} {...props}
/> />
) );
} }
function BreadcrumbLink({ function BreadcrumbLink({
@@ -36,9 +36,9 @@ function BreadcrumbLink({
className, className,
...props ...props
}: React.ComponentProps<"a"> & { }: React.ComponentProps<"a"> & {
asChild?: boolean asChild?: boolean;
}) { }) {
const Comp = asChild ? Slot : "a" const Comp = asChild ? Slot : "a";
return ( return (
<Comp <Comp
@@ -46,7 +46,7 @@ function BreadcrumbLink({
className={cn("hover:text-foreground transition-colors", className)} className={cn("hover:text-foreground transition-colors", className)}
{...props} {...props}
/> />
) );
} }
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) { function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
@@ -59,7 +59,7 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
className={cn("text-foreground font-normal", className)} className={cn("text-foreground font-normal", className)}
{...props} {...props}
/> />
) );
} }
function BreadcrumbSeparator({ function BreadcrumbSeparator({
@@ -77,7 +77,7 @@ function BreadcrumbSeparator({
> >
{children ?? <ChevronRight />} {children ?? <ChevronRight />}
</li> </li>
) );
} }
function BreadcrumbEllipsis({ function BreadcrumbEllipsis({
@@ -95,7 +95,7 @@ function BreadcrumbEllipsis({
<MoreHorizontal className="size-4" /> <MoreHorizontal className="size-4" />
<span className="sr-only">More</span> <span className="sr-only">More</span>
</span> </span>
) );
} }
export { export {
@@ -106,4 +106,4 @@ export {
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
BreadcrumbEllipsis, BreadcrumbEllipsis,
} };

View File

@@ -1,8 +1,7 @@
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator"
const buttonGroupVariants = cva( const buttonGroupVariants = cva(
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2", "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
@@ -18,8 +17,8 @@ const buttonGroupVariants = cva(
defaultVariants: { defaultVariants: {
orientation: "horizontal", orientation: "horizontal",
}, },
} },
) );
function ButtonGroup({ function ButtonGroup({
className, className,
@@ -34,7 +33,7 @@ function ButtonGroup({
className={cn(buttonGroupVariants({ orientation }), className)} className={cn(buttonGroupVariants({ orientation }), className)}
{...props} {...props}
/> />
) );
} }
function ButtonGroupText({ function ButtonGroupText({
@@ -42,19 +41,19 @@ function ButtonGroupText({
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
asChild?: boolean asChild?: boolean;
}) { }) {
const Comp = asChild ? Slot : "div" const Comp = asChild ? Slot : "div";
return ( return (
<Comp <Comp
className={cn( className={cn(
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", "bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ButtonGroupSeparator({ function ButtonGroupSeparator({
@@ -68,11 +67,11 @@ function ButtonGroupSeparator({
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto", "bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -80,4 +79,4 @@ export {
ButtonGroupSeparator, ButtonGroupSeparator,
ButtonGroupText, ButtonGroupText,
buttonGroupVariants, buttonGroupVariants,
} };

View File

@@ -1,8 +1,8 @@
import * as React from "react" import { Slot } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const buttonVariants = cva( 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", "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",
@@ -14,8 +14,7 @@ const buttonVariants = cva(
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: 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", "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
@@ -33,8 +32,8 @@ const buttonVariants = cva(
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) );
function Button({ function Button({
className, className,
@@ -44,9 +43,9 @@ function Button({
...props ...props
}: React.ComponentProps<"button"> & }: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean asChild?: boolean;
}) { }) {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
@@ -56,7 +55,7 @@ function Button({
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
{...props} {...props}
/> />
) );
} }
export { Button, buttonVariants } export { Button, buttonVariants };

View File

@@ -1,19 +1,18 @@
"use client" "use client";
import * as React from "react"
import { import {
ChevronDownIcon, ChevronDownIcon,
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
} from "lucide-react" } from "lucide-react";
import * as React from "react";
import { import {
type DayButton,
DayPicker, DayPicker,
getDefaultClassNames, getDefaultClassNames,
type DayButton, } from "react-day-picker";
} from "react-day-picker" import { Button, buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({ function Calendar({
className, className,
@@ -25,9 +24,9 @@ function Calendar({
components, components,
...props ...props
}: React.ComponentProps<typeof DayPicker> & { }: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"] buttonVariant?: React.ComponentProps<typeof Button>["variant"];
}) { }) {
const defaultClassNames = getDefaultClassNames() const defaultClassNames = getDefaultClassNames();
return ( return (
<DayPicker <DayPicker
@@ -36,7 +35,7 @@ function Calendar({
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent", "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className className,
)} )}
captionLayout={captionLayout} captionLayout={captionLayout}
formatters={{ formatters={{
@@ -48,85 +47,82 @@ function Calendar({
root: cn("w-fit", defaultClassNames.root), root: cn("w-fit", defaultClassNames.root),
months: cn( months: cn(
"flex gap-4 flex-col md:flex-row relative", "flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months defaultClassNames.months,
), ),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month), month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn( nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav defaultClassNames.nav,
), ),
button_previous: cn( button_previous: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous defaultClassNames.button_previous,
), ),
button_next: cn( button_next: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next defaultClassNames.button_next,
), ),
month_caption: cn( month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption defaultClassNames.month_caption,
), ),
dropdowns: cn( dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns defaultClassNames.dropdowns,
), ),
dropdown_root: cn( dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root defaultClassNames.dropdown_root,
), ),
dropdown: cn( dropdown: cn(
"absolute bg-popover inset-0 opacity-0", "absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown defaultClassNames.dropdown,
), ),
caption_label: cn( caption_label: cn(
"select-none font-medium", "select-none font-medium",
captionLayout === "label" captionLayout === "label"
? "text-sm" ? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label defaultClassNames.caption_label,
), ),
table: "w-full border-collapse", table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays), weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn( weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday defaultClassNames.weekday,
), ),
week: cn("flex w-full mt-2", defaultClassNames.week), week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn( week_number_header: cn(
"select-none w-(--cell-size)", "select-none w-(--cell-size)",
defaultClassNames.week_number_header defaultClassNames.week_number_header,
), ),
week_number: cn( week_number: cn(
"text-[0.8rem] select-none text-muted-foreground", "text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number defaultClassNames.week_number,
), ),
day: cn( day: cn(
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", "relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
props.showWeekNumber props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md" ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
: "[&:first-child[data-selected=true]_button]:rounded-l-md", : "[&:first-child[data-selected=true]_button]:rounded-l-md",
defaultClassNames.day defaultClassNames.day,
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
), ),
range_start: cn("rounded-l-md bg-accent", defaultClassNames.range_start),
range_middle: cn("rounded-none", defaultClassNames.range_middle), range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn( today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today defaultClassNames.today,
), ),
outside: cn( outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground", "text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside defaultClassNames.outside,
), ),
disabled: cn( disabled: cn(
"text-muted-foreground opacity-50", "text-muted-foreground opacity-50",
defaultClassNames.disabled defaultClassNames.disabled,
), ),
hidden: cn("invisible", defaultClassNames.hidden), hidden: cn("invisible", defaultClassNames.hidden),
...classNames, ...classNames,
@@ -140,27 +136,20 @@ function Calendar({
className={cn(className)} className={cn(className)}
{...props} {...props}
/> />
) );
}, },
Chevron: ({ className, orientation, ...props }) => { Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") { if (orientation === "left") {
return ( return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
} }
if (orientation === "right") { if (orientation === "right") {
return ( return (
<ChevronRightIcon <ChevronRightIcon className={cn("size-4", className)} {...props} />
className={cn("size-4", className)} );
{...props}
/>
)
} }
return ( return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
}, },
DayButton: CalendarDayButton, DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => { WeekNumber: ({ children, ...props }) => {
@@ -170,13 +159,13 @@ function Calendar({
{children} {children}
</div> </div>
</td> </td>
) );
}, },
...components, ...components,
}} }}
{...props} {...props}
/> />
) );
} }
function CalendarDayButton({ function CalendarDayButton({
@@ -185,12 +174,12 @@ function CalendarDayButton({
modifiers, modifiers,
...props ...props
}: React.ComponentProps<typeof DayButton>) { }: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames() const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null) const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => { React.useEffect(() => {
if (modifiers.focused) ref.current?.focus() if (modifiers.focused) ref.current?.focus();
}, [modifiers.focused]) }, [modifiers.focused]);
return ( return (
<Button <Button
@@ -210,11 +199,11 @@ function CalendarDayButton({
className={cn( className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70", "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day, defaultClassNames.day,
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Calendar, CalendarDayButton } export { Calendar, CalendarDayButton };

View File

@@ -1,6 +1,6 @@
import * as React from "react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) { function Card({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
@@ -8,11 +8,11 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card" data-slot="card"
className={cn( className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CardHeader({ className, ...props }: React.ComponentProps<"div">) { function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -21,11 +21,11 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-header" data-slot="card-header"
className={cn( className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CardTitle({ className, ...props }: React.ComponentProps<"div">) { function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -35,7 +35,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
className={cn("leading-none font-semibold", className)} className={cn("leading-none font-semibold", className)}
{...props} {...props}
/> />
) );
} }
function CardDescription({ className, ...props }: React.ComponentProps<"div">) { function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
@@ -45,7 +45,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) );
} }
function CardAction({ className, ...props }: React.ComponentProps<"div">) { function CardAction({ className, ...props }: React.ComponentProps<"div">) {
@@ -54,21 +54,17 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
data-slot="card-action" data-slot="card-action"
className={cn( className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end", "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CardContent({ className, ...props }: React.ComponentProps<"div">) { function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div data-slot="card-content" className={cn("px-6", className)} {...props} />
data-slot="card-content" );
className={cn("px-6", className)}
{...props}
/>
)
} }
function CardFooter({ className, ...props }: React.ComponentProps<"div">) { function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -78,7 +74,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex items-center px-6 [.border-t]:pt-6", className)} className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -89,4 +85,4 @@ export {
CardAction, CardAction,
CardDescription, CardDescription,
CardContent, CardContent,
} };

View File

@@ -1,45 +1,44 @@
"use client" "use client";
import * as React from "react"
import useEmblaCarousel, { import useEmblaCarousel, {
type UseEmblaCarouselType, type UseEmblaCarouselType,
} from "embla-carousel-react" } from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react" import { ArrowLeft, ArrowRight } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils" type CarouselApi = UseEmblaCarouselType[1];
import { Button } from "@/components/ui/button" type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselApi = UseEmblaCarouselType[1] type CarouselPlugin = UseCarouselParameters[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = { type CarouselProps = {
opts?: CarouselOptions opts?: CarouselOptions;
plugins?: CarouselPlugin plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical" orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void setApi?: (api: CarouselApi) => void;
} };
type CarouselContextProps = { type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0] carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1] api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void scrollPrev: () => void;
scrollNext: () => void scrollNext: () => void;
canScrollPrev: boolean canScrollPrev: boolean;
canScrollNext: boolean canScrollNext: boolean;
} & CarouselProps } & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null) const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() { function useCarousel() {
const context = React.useContext(CarouselContext) const context = React.useContext(CarouselContext);
if (!context) { if (!context) {
throw new Error("useCarousel must be used within a <Carousel />") throw new Error("useCarousel must be used within a <Carousel />");
} }
return context return context;
} }
function Carousel({ function Carousel({
@@ -56,53 +55,53 @@ function Carousel({
...opts, ...opts,
axis: orientation === "horizontal" ? "x" : "y", axis: orientation === "horizontal" ? "x" : "y",
}, },
plugins plugins,
) );
const [canScrollPrev, setCanScrollPrev] = React.useState(false) const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false) const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => { const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return if (!api) return;
setCanScrollPrev(api.canScrollPrev()) setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext()) setCanScrollNext(api.canScrollNext());
}, []) }, []);
const scrollPrev = React.useCallback(() => { const scrollPrev = React.useCallback(() => {
api?.scrollPrev() api?.scrollPrev();
}, [api]) }, [api]);
const scrollNext = React.useCallback(() => { const scrollNext = React.useCallback(() => {
api?.scrollNext() api?.scrollNext();
}, [api]) }, [api]);
const handleKeyDown = React.useCallback( const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") { if (event.key === "ArrowLeft") {
event.preventDefault() event.preventDefault();
scrollPrev() scrollPrev();
} else if (event.key === "ArrowRight") { } else if (event.key === "ArrowRight") {
event.preventDefault() event.preventDefault();
scrollNext() scrollNext();
} }
}, },
[scrollPrev, scrollNext] [scrollPrev, scrollNext],
) );
React.useEffect(() => { React.useEffect(() => {
if (!api || !setApi) return if (!api || !setApi) return;
setApi(api) setApi(api);
}, [api, setApi]) }, [api, setApi]);
React.useEffect(() => { React.useEffect(() => {
if (!api) return if (!api) return;
onSelect(api) onSelect(api);
api.on("reInit", onSelect) api.on("reInit", onSelect);
api.on("select", onSelect) api.on("select", onSelect);
return () => { return () => {
api?.off("select", onSelect) api?.off("select", onSelect);
} };
}, [api, onSelect]) }, [api, onSelect]);
return ( return (
<CarouselContext.Provider <CarouselContext.Provider
@@ -129,11 +128,11 @@ function Carousel({
{children} {children}
</div> </div>
</CarouselContext.Provider> </CarouselContext.Provider>
) );
} }
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) { function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel() const { carouselRef, orientation } = useCarousel();
return ( return (
<div <div
@@ -145,16 +144,16 @@ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
className={cn( className={cn(
"flex", "flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className className,
)} )}
{...props} {...props}
/> />
</div> </div>
) );
} }
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) { function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel() const { orientation } = useCarousel();
return ( return (
<div <div
@@ -164,11 +163,11 @@ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
className={cn( className={cn(
"min-w-0 shrink-0 grow-0 basis-full", "min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4", orientation === "horizontal" ? "pl-4" : "pt-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CarouselPrevious({ function CarouselPrevious({
@@ -177,7 +176,7 @@ function CarouselPrevious({
size = "icon", size = "icon",
...props ...props
}: React.ComponentProps<typeof Button>) { }: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel() const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return ( return (
<Button <Button
@@ -189,7 +188,7 @@ function CarouselPrevious({
orientation === "horizontal" orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2" ? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className className,
)} )}
disabled={!canScrollPrev} disabled={!canScrollPrev}
onClick={scrollPrev} onClick={scrollPrev}
@@ -198,7 +197,7 @@ function CarouselPrevious({
<ArrowLeft /> <ArrowLeft />
<span className="sr-only">Previous slide</span> <span className="sr-only">Previous slide</span>
</Button> </Button>
) );
} }
function CarouselNext({ function CarouselNext({
@@ -207,7 +206,7 @@ function CarouselNext({
size = "icon", size = "icon",
...props ...props
}: React.ComponentProps<typeof Button>) { }: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel() const { orientation, scrollNext, canScrollNext } = useCarousel();
return ( return (
<Button <Button
@@ -219,7 +218,7 @@ function CarouselNext({
orientation === "horizontal" orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2" ? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className className,
)} )}
disabled={!canScrollNext} disabled={!canScrollNext}
onClick={scrollNext} onClick={scrollNext}
@@ -228,7 +227,7 @@ function CarouselNext({
<ArrowRight /> <ArrowRight />
<span className="sr-only">Next slide</span> <span className="sr-only">Next slide</span>
</Button> </Button>
) );
} }
export { export {
@@ -238,4 +237,4 @@ export {
CarouselItem, CarouselItem,
CarouselPrevious, CarouselPrevious,
CarouselNext, CarouselNext,
} };

View File

@@ -1,37 +1,37 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import * as RechartsPrimitive from "recharts" import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR } // Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = { export type ChartConfig = {
[k in string]: { [k in string]: {
label?: React.ReactNode label?: React.ReactNode;
icon?: React.ComponentType icon?: React.ComponentType;
} & ( } & (
| { color?: string; theme?: never } | { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> } | { color?: never; theme: Record<keyof typeof THEMES, string> }
) );
} };
type ChartContextProps = { type ChartContextProps = {
config: ChartConfig config: ChartConfig;
} };
const ChartContext = React.createContext<ChartContextProps | null>(null) const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() { function useChart() {
const context = React.useContext(ChartContext) const context = React.useContext(ChartContext);
if (!context) { if (!context) {
throw new Error("useChart must be used within a <ChartContainer />") throw new Error("useChart must be used within a <ChartContainer />");
} }
return context return context;
} }
function ChartContainer({ function ChartContainer({
@@ -41,13 +41,13 @@ function ChartContainer({
config, config,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
config: ChartConfig config: ChartConfig;
children: React.ComponentProps< children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer typeof RechartsPrimitive.ResponsiveContainer
>["children"] >["children"];
}) { }) {
const uniqueId = React.useId() const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return ( return (
<ChartContext.Provider value={{ config }}> <ChartContext.Provider value={{ config }}>
@@ -56,7 +56,7 @@ function ChartContainer({
data-chart={chartId} data-chart={chartId}
className={cn( className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden", "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className className,
)} )}
{...props} {...props}
> >
@@ -66,20 +66,21 @@ function ChartContainer({
</RechartsPrimitive.ResponsiveContainer> </RechartsPrimitive.ResponsiveContainer>
</div> </div>
</ChartContext.Provider> </ChartContext.Provider>
) );
} }
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter( const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color ([, config]) => config.theme || config.color,
) );
if (!colorConfig.length) { if (!colorConfig.length) {
return null return null;
} }
return ( return (
<style <style
// biome-ignore lint/security/noDangerouslySetInnerHtml: no comment...
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: Object.entries(THEMES) __html: Object.entries(THEMES)
.map( .map(
@@ -89,20 +90,20 @@ ${colorConfig
.map(([key, itemConfig]) => { .map(([key, itemConfig]) => {
const color = const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color itemConfig.color;
return color ? ` --color-${key}: ${color};` : null return color ? ` --color-${key}: ${color};` : null;
}) })
.join("\n")} .join("\n")}
} }
` `,
) )
.join("\n"), .join("\n"),
}} }}
/> />
) );
} };
const ChartTooltip = RechartsPrimitive.Tooltip const ChartTooltip = RechartsPrimitive.Tooltip;
function ChartTooltipContent({ function ChartTooltipContent({
active, active,
@@ -120,40 +121,40 @@ function ChartTooltipContent({
labelKey, labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> & }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
hideLabel?: boolean hideLabel?: boolean;
hideIndicator?: boolean hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed" indicator?: "line" | "dot" | "dashed";
nameKey?: string nameKey?: string;
labelKey?: string labelKey?: string;
}) { }) {
const { config } = useChart() const { config } = useChart();
const tooltipLabel = React.useMemo(() => { const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) { if (hideLabel || !payload?.length) {
return null return null;
} }
const [item] = payload const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}` const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value = const value =
!labelKey && typeof label === "string" !labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label ? config[label as keyof typeof config]?.label || label
: itemConfig?.label : itemConfig?.label;
if (labelFormatter) { if (labelFormatter) {
return ( return (
<div className={cn("font-medium", labelClassName)}> <div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)} {labelFormatter(value, payload)}
</div> </div>
) );
} }
if (!value) { if (!value) {
return null return null;
} }
return <div className={cn("font-medium", labelClassName)}>{value}</div> return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [ }, [
label, label,
labelFormatter, labelFormatter,
@@ -162,19 +163,19 @@ function ChartTooltipContent({
labelClassName, labelClassName,
config, config,
labelKey, labelKey,
]) ]);
if (!active || !payload?.length) { if (!active || !payload?.length) {
return null return null;
} }
const nestLabel = payload.length === 1 && indicator !== "dot" const nestLabel = payload.length === 1 && indicator !== "dot";
return ( return (
<div <div
className={cn( className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl", "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className className,
)} )}
> >
{!nestLabel ? tooltipLabel : null} {!nestLabel ? tooltipLabel : null}
@@ -182,16 +183,16 @@ function ChartTooltipContent({
{payload {payload
.filter((item) => item.type !== "none") .filter((item) => item.type !== "none")
.map((item, index) => { .map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}` const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color const indicatorColor = color || item.payload.fill || item.color;
return ( return (
<div <div
key={item.dataKey} key={item.dataKey}
className={cn( className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5", "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center" indicator === "dot" && "items-center",
)} )}
> >
{formatter && item?.value !== undefined && item.name ? ( {formatter && item?.value !== undefined && item.name ? (
@@ -211,7 +212,7 @@ function ChartTooltipContent({
"w-0 border-[1.5px] border-dashed bg-transparent": "w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed", indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed", "my-0.5": nestLabel && indicator === "dashed",
} },
)} )}
style={ style={
{ {
@@ -225,7 +226,7 @@ function ChartTooltipContent({
<div <div
className={cn( className={cn(
"flex flex-1 justify-between leading-none", "flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center" nestLabel ? "items-end" : "items-center",
)} )}
> >
<div className="grid gap-1.5"> <div className="grid gap-1.5">
@@ -243,14 +244,14 @@ function ChartTooltipContent({
</> </>
)} )}
</div> </div>
) );
})} })}
</div> </div>
</div> </div>
) );
} }
const ChartLegend = RechartsPrimitive.Legend const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({ function ChartLegendContent({
className, className,
@@ -260,13 +261,13 @@ function ChartLegendContent({
nameKey, nameKey,
}: React.ComponentProps<"div"> & }: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean hideIcon?: boolean;
nameKey?: string nameKey?: string;
}) { }) {
const { config } = useChart() const { config } = useChart();
if (!payload?.length) { if (!payload?.length) {
return null return null;
} }
return ( return (
@@ -274,20 +275,20 @@ function ChartLegendContent({
className={cn( className={cn(
"flex items-center justify-center gap-4", "flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3", verticalAlign === "top" ? "pb-3" : "pt-3",
className className,
)} )}
> >
{payload {payload
.filter((item) => item.type !== "none") .filter((item) => item.type !== "none")
.map((item) => { .map((item) => {
const key = `${nameKey || item.dataKey || "value"}` const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key) const itemConfig = getPayloadConfigFromPayload(config, item, key);
return ( return (
<div <div
key={item.value} key={item.value}
className={cn( className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3" "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
)} )}
> >
{itemConfig?.icon && !hideIcon ? ( {itemConfig?.icon && !hideIcon ? (
@@ -302,20 +303,20 @@ function ChartLegendContent({
)} )}
{itemConfig?.label} {itemConfig?.label}
</div> </div>
) );
})} })}
</div> </div>
) );
} }
// Helper to extract item config from a payload. // Helper to extract item config from a payload.
function getPayloadConfigFromPayload( function getPayloadConfigFromPayload(
config: ChartConfig, config: ChartConfig,
payload: unknown, payload: unknown,
key: string key: string,
) { ) {
if (typeof payload !== "object" || payload === null) { if (typeof payload !== "object" || payload === null) {
return undefined return undefined;
} }
const payloadPayload = const payloadPayload =
@@ -323,28 +324,26 @@ function getPayloadConfigFromPayload(
typeof payload.payload === "object" && typeof payload.payload === "object" &&
payload.payload !== null payload.payload !== null
? payload.payload ? payload.payload
: undefined : undefined;
let configLabelKey: string = key let configLabelKey: string = key;
if ( if (
key in payload && key in payload &&
typeof payload[key as keyof typeof payload] === "string" typeof payload[key as keyof typeof payload] === "string"
) { ) {
configLabelKey = payload[key as keyof typeof payload] as string configLabelKey = payload[key as keyof typeof payload] as string;
} else if ( } else if (
payloadPayload && payloadPayload &&
key in payloadPayload && key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string" typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) { ) {
configLabelKey = payloadPayload[ configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
key as keyof typeof payloadPayload
] as string
} }
return configLabelKey in config return configLabelKey in config
? config[configLabelKey] ? config[configLabelKey]
: config[key as keyof typeof config] : config[key as keyof typeof config];
} }
export { export {
@@ -354,4 +353,4 @@ export {
ChartLegend, ChartLegend,
ChartLegendContent, ChartLegendContent,
ChartStyle, ChartStyle,
} };

View File

@@ -1,10 +1,10 @@
"use client" "use client";
import * as React from "react" import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import { CheckIcon } from "lucide-react";
import { CheckIcon } from "lucide-react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Checkbox({ function Checkbox({
className, className,
@@ -15,7 +15,7 @@ function Checkbox({
data-slot="checkbox" data-slot="checkbox"
className={cn( className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >
@@ -26,7 +26,7 @@ function Checkbox({
<CheckIcon className="size-3.5" /> <CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator> </CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>
) );
} }
export { Checkbox } export { Checkbox };

View File

@@ -1,11 +1,11 @@
"use client" "use client";
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
function Collapsible({ function Collapsible({
...props ...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) { }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} /> return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
} }
function CollapsibleTrigger({ function CollapsibleTrigger({
@@ -16,7 +16,7 @@ function CollapsibleTrigger({
data-slot="collapsible-trigger" data-slot="collapsible-trigger"
{...props} {...props}
/> />
) );
} }
function CollapsibleContent({ function CollapsibleContent({
@@ -27,7 +27,7 @@ function CollapsibleContent({
data-slot="collapsible-content" data-slot="collapsible-content"
{...props} {...props}
/> />
) );
} }
export { Collapsible, CollapsibleTrigger, CollapsibleContent } export { Collapsible, CollapsibleTrigger, CollapsibleContent };

View File

@@ -1,17 +1,16 @@
"use client" "use client";
import * as React from "react" import { Command as CommandPrimitive } from "cmdk";
import { Command as CommandPrimitive } from "cmdk" import { SearchIcon } from "lucide-react";
import { SearchIcon } from "lucide-react" import type * as React from "react";
import { cn } from "@/lib/utils"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
function Command({ function Command({
className, className,
@@ -22,11 +21,11 @@ function Command({
data-slot="command" data-slot="command"
className={cn( className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md", "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CommandDialog({ function CommandDialog({
@@ -37,10 +36,10 @@ function CommandDialog({
showCloseButton = true, showCloseButton = true,
...props ...props
}: React.ComponentProps<typeof Dialog> & { }: React.ComponentProps<typeof Dialog> & {
title?: string title?: string;
description?: string description?: string;
className?: string className?: string;
showCloseButton?: boolean showCloseButton?: boolean;
}) { }) {
return ( return (
<Dialog {...props}> <Dialog {...props}>
@@ -57,7 +56,7 @@ function CommandDialog({
</Command> </Command>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) );
} }
function CommandInput({ function CommandInput({
@@ -74,12 +73,12 @@ function CommandInput({
data-slot="command-input" data-slot="command-input"
className={cn( className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50", "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
/> />
</div> </div>
) );
} }
function CommandList({ function CommandList({
@@ -91,11 +90,11 @@ function CommandList({
data-slot="command-list" data-slot="command-list"
className={cn( className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CommandEmpty({ function CommandEmpty({
@@ -107,7 +106,7 @@ function CommandEmpty({
className="py-6 text-center text-sm" className="py-6 text-center text-sm"
{...props} {...props}
/> />
) );
} }
function CommandGroup({ function CommandGroup({
@@ -119,11 +118,11 @@ function CommandGroup({
data-slot="command-group" data-slot="command-group"
className={cn( className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium", "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CommandSeparator({ function CommandSeparator({
@@ -136,7 +135,7 @@ function CommandSeparator({
className={cn("bg-border -mx-1 h-px", className)} className={cn("bg-border -mx-1 h-px", className)}
{...props} {...props}
/> />
) );
} }
function CommandItem({ function CommandItem({
@@ -148,11 +147,11 @@ function CommandItem({
data-slot="command-item" data-slot="command-item"
className={cn( className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function CommandShortcut({ function CommandShortcut({
@@ -164,11 +163,11 @@ function CommandShortcut({
data-slot="command-shortcut" data-slot="command-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -181,4 +180,4 @@ export {
CommandItem, CommandItem,
CommandShortcut, CommandShortcut,
CommandSeparator, CommandSeparator,
} };

View File

@@ -1,15 +1,15 @@
"use client" "use client";
import * as React from "react" import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function ContextMenu({ function ContextMenu({
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} /> return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
} }
function ContextMenuTrigger({ function ContextMenuTrigger({
@@ -17,7 +17,7 @@ function ContextMenuTrigger({
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return ( return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} /> <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
) );
} }
function ContextMenuGroup({ function ContextMenuGroup({
@@ -25,7 +25,7 @@ function ContextMenuGroup({
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return ( return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} /> <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
) );
} }
function ContextMenuPortal({ function ContextMenuPortal({
@@ -33,13 +33,13 @@ function ContextMenuPortal({
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return ( return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} /> <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
) );
} }
function ContextMenuSub({ function ContextMenuSub({
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) { }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} /> return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
} }
function ContextMenuRadioGroup({ function ContextMenuRadioGroup({
@@ -50,7 +50,7 @@ function ContextMenuRadioGroup({
data-slot="context-menu-radio-group" data-slot="context-menu-radio-group"
{...props} {...props}
/> />
) );
} }
function ContextMenuSubTrigger({ function ContextMenuSubTrigger({
@@ -59,7 +59,7 @@ function ContextMenuSubTrigger({
children, children,
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & { }: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<ContextMenuPrimitive.SubTrigger <ContextMenuPrimitive.SubTrigger
@@ -67,14 +67,14 @@ function ContextMenuSubTrigger({
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto" /> <ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger> </ContextMenuPrimitive.SubTrigger>
) );
} }
function ContextMenuSubContent({ function ContextMenuSubContent({
@@ -86,11 +86,11 @@ function ContextMenuSubContent({
data-slot="context-menu-sub-content" data-slot="context-menu-sub-content"
className={cn( 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 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", "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 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ContextMenuContent({ function ContextMenuContent({
@@ -103,12 +103,12 @@ function ContextMenuContent({
data-slot="context-menu-content" data-slot="context-menu-content"
className={cn( 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-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", "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-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className className,
)} )}
{...props} {...props}
/> />
</ContextMenuPrimitive.Portal> </ContextMenuPrimitive.Portal>
) );
} }
function ContextMenuItem({ function ContextMenuItem({
@@ -117,8 +117,8 @@ function ContextMenuItem({
variant = "default", variant = "default",
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & { }: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
variant?: "default" | "destructive" variant?: "default" | "destructive";
}) { }) {
return ( return (
<ContextMenuPrimitive.Item <ContextMenuPrimitive.Item
@@ -127,11 +127,11 @@ function ContextMenuItem({
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ContextMenuCheckboxItem({ function ContextMenuCheckboxItem({
@@ -145,7 +145,7 @@ function ContextMenuCheckboxItem({
data-slot="context-menu-checkbox-item" data-slot="context-menu-checkbox-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
@@ -157,7 +157,7 @@ function ContextMenuCheckboxItem({
</span> </span>
{children} {children}
</ContextMenuPrimitive.CheckboxItem> </ContextMenuPrimitive.CheckboxItem>
) );
} }
function ContextMenuRadioItem({ function ContextMenuRadioItem({
@@ -170,7 +170,7 @@ function ContextMenuRadioItem({
data-slot="context-menu-radio-item" data-slot="context-menu-radio-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
> >
@@ -181,7 +181,7 @@ function ContextMenuRadioItem({
</span> </span>
{children} {children}
</ContextMenuPrimitive.RadioItem> </ContextMenuPrimitive.RadioItem>
) );
} }
function ContextMenuLabel({ function ContextMenuLabel({
@@ -189,7 +189,7 @@ function ContextMenuLabel({
inset, inset,
...props ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & { }: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<ContextMenuPrimitive.Label <ContextMenuPrimitive.Label
@@ -197,11 +197,11 @@ function ContextMenuLabel({
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", "text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ContextMenuSeparator({ function ContextMenuSeparator({
@@ -214,7 +214,7 @@ function ContextMenuSeparator({
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props} {...props}
/> />
) );
} }
function ContextMenuShortcut({ function ContextMenuShortcut({
@@ -226,11 +226,11 @@ function ContextMenuShortcut({
data-slot="context-menu-shortcut" data-slot="context-menu-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -249,4 +249,4 @@ export {
ContextMenuSubContent, ContextMenuSubContent,
ContextMenuSubTrigger, ContextMenuSubTrigger,
ContextMenuRadioGroup, ContextMenuRadioGroup,
} };

View File

@@ -1,33 +1,33 @@
"use client" "use client";
import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as DialogPrimitive from "@radix-ui/react-dialog" import { XIcon } from "lucide-react";
import { XIcon } from "lucide-react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Dialog({ function Dialog({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) { }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} /> return <DialogPrimitive.Root data-slot="dialog" {...props} />;
} }
function DialogTrigger({ function DialogTrigger({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) { }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
} }
function DialogPortal({ function DialogPortal({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) { }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} /> return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
} }
function DialogClose({ function DialogClose({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) { }: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} /> return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
} }
function DialogOverlay({ function DialogOverlay({
@@ -39,11 +39,11 @@ function DialogOverlay({
data-slot="dialog-overlay" data-slot="dialog-overlay"
className={cn( className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DialogContent({ function DialogContent({
@@ -52,7 +52,7 @@ function DialogContent({
showCloseButton = true, showCloseButton = true,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & { }: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean showCloseButton?: boolean;
}) { }) {
return ( return (
<DialogPortal data-slot="dialog-portal"> <DialogPortal data-slot="dialog-portal">
@@ -61,7 +61,7 @@ function DialogContent({
data-slot="dialog-content" data-slot="dialog-content"
className={cn( className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg", "bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className className,
)} )}
{...props} {...props}
> >
@@ -77,7 +77,7 @@ function DialogContent({
)} )}
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>
) );
} }
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -87,7 +87,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 text-center sm:text-left", className)} className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props} {...props}
/> />
) );
} }
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -96,11 +96,11 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
data-slot="dialog-footer" data-slot="dialog-footer"
className={cn( className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DialogTitle({ function DialogTitle({
@@ -113,7 +113,7 @@ function DialogTitle({
className={cn("text-lg leading-none font-semibold", className)} className={cn("text-lg leading-none font-semibold", className)}
{...props} {...props}
/> />
) );
} }
function DialogDescription({ function DialogDescription({
@@ -126,7 +126,7 @@ function DialogDescription({
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -140,4 +140,4 @@ export {
DialogPortal, DialogPortal,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} };

View File

@@ -1,32 +1,32 @@
"use client" "use client";
import * as React from "react" import type * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul" import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Drawer({ function Drawer({
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) { }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} /> return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
} }
function DrawerTrigger({ function DrawerTrigger({
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) { }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} /> return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
} }
function DrawerPortal({ function DrawerPortal({
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) { }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} /> return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
} }
function DrawerClose({ function DrawerClose({
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) { }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} /> return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
} }
function DrawerOverlay({ function DrawerOverlay({
@@ -38,11 +38,11 @@ function DrawerOverlay({
data-slot="drawer-overlay" data-slot="drawer-overlay"
className={cn( className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DrawerContent({ function DrawerContent({
@@ -61,7 +61,7 @@ function DrawerContent({
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t", "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm", "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm", "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className className,
)} )}
{...props} {...props}
> >
@@ -69,7 +69,7 @@ function DrawerContent({
{children} {children}
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
</DrawerPortal> </DrawerPortal>
) );
} }
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -78,11 +78,11 @@ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="drawer-header" data-slot="drawer-header"
className={cn( className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left", "flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -92,7 +92,7 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("mt-auto flex flex-col gap-2 p-4", className)} className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props} {...props}
/> />
) );
} }
function DrawerTitle({ function DrawerTitle({
@@ -105,7 +105,7 @@ function DrawerTitle({
className={cn("text-foreground font-semibold", className)} className={cn("text-foreground font-semibold", className)}
{...props} {...props}
/> />
) );
} }
function DrawerDescription({ function DrawerDescription({
@@ -118,7 +118,7 @@ function DrawerDescription({
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -132,4 +132,4 @@ export {
DrawerFooter, DrawerFooter,
DrawerTitle, DrawerTitle,
DrawerDescription, DrawerDescription,
} };

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 * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function DropdownMenu({ function DropdownMenu({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
} }
function DropdownMenuPortal({ function DropdownMenuPortal({
@@ -17,18 +17,15 @@ function DropdownMenuPortal({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return ( return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
) );
} }
function DropdownMenuTrigger({ function DropdownMenuTrigger({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return ( return (
<DropdownMenuPrimitive.Trigger <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
data-slot="dropdown-menu-trigger" );
{...props}
/>
)
} }
function DropdownMenuContent({ function DropdownMenuContent({
@@ -43,12 +40,12 @@ function DropdownMenuContent({
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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 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",
className className,
)} )}
{...props} {...props}
/> />
</DropdownMenuPrimitive.Portal> </DropdownMenuPrimitive.Portal>
) );
} }
function DropdownMenuGroup({ function DropdownMenuGroup({
@@ -56,7 +53,7 @@ function DropdownMenuGroup({
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return ( return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
) );
} }
function DropdownMenuItem({ function DropdownMenuItem({
@@ -65,8 +62,8 @@ function DropdownMenuItem({
variant = "default", variant = "default",
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean;
variant?: "default" | "destructive" variant?: "default" | "destructive";
}) { }) {
return ( return (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
@@ -75,11 +72,11 @@ function DropdownMenuItem({
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DropdownMenuCheckboxItem({ function DropdownMenuCheckboxItem({
@@ -93,7 +90,7 @@ function DropdownMenuCheckboxItem({
data-slot="dropdown-menu-checkbox-item" data-slot="dropdown-menu-checkbox-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
@@ -105,7 +102,7 @@ function DropdownMenuCheckboxItem({
</span> </span>
{children} {children}
</DropdownMenuPrimitive.CheckboxItem> </DropdownMenuPrimitive.CheckboxItem>
) );
} }
function DropdownMenuRadioGroup({ function DropdownMenuRadioGroup({
@@ -116,7 +113,7 @@ function DropdownMenuRadioGroup({
data-slot="dropdown-menu-radio-group" data-slot="dropdown-menu-radio-group"
{...props} {...props}
/> />
) );
} }
function DropdownMenuRadioItem({ function DropdownMenuRadioItem({
@@ -129,7 +126,7 @@ function DropdownMenuRadioItem({
data-slot="dropdown-menu-radio-item" data-slot="dropdown-menu-radio-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
> >
@@ -140,7 +137,7 @@ function DropdownMenuRadioItem({
</span> </span>
{children} {children}
</DropdownMenuPrimitive.RadioItem> </DropdownMenuPrimitive.RadioItem>
) );
} }
function DropdownMenuLabel({ function DropdownMenuLabel({
@@ -148,7 +145,7 @@ function DropdownMenuLabel({
inset, inset,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
@@ -156,11 +153,11 @@ function DropdownMenuLabel({
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DropdownMenuSeparator({ function DropdownMenuSeparator({
@@ -173,7 +170,7 @@ function DropdownMenuSeparator({
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props} {...props}
/> />
) );
} }
function DropdownMenuShortcut({ function DropdownMenuShortcut({
@@ -185,17 +182,17 @@ function DropdownMenuShortcut({
data-slot="dropdown-menu-shortcut" data-slot="dropdown-menu-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function DropdownMenuSub({ function DropdownMenuSub({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { }: 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({ function DropdownMenuSubTrigger({
@@ -204,7 +201,7 @@ function DropdownMenuSubTrigger({
children, children,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
@@ -212,14 +209,14 @@ function DropdownMenuSubTrigger({
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto size-4" /> <ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger> </DropdownMenuPrimitive.SubTrigger>
) );
} }
function DropdownMenuSubContent({ function DropdownMenuSubContent({
@@ -231,11 +228,11 @@ function DropdownMenuSubContent({
data-slot="dropdown-menu-sub-content" data-slot="dropdown-menu-sub-content"
className={cn( 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 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", "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 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -254,4 +251,4 @@ export {
DropdownMenuSub, DropdownMenuSub,
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuSubContent, DropdownMenuSubContent,
} };

View File

@@ -1,6 +1,6 @@
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Empty({ className, ...props }: React.ComponentProps<"div">) { function Empty({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
@@ -8,11 +8,11 @@ function Empty({ className, ...props }: React.ComponentProps<"div">) {
data-slot="empty" data-slot="empty"
className={cn( className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12", "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -21,11 +21,11 @@ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="empty-header" data-slot="empty-header"
className={cn( className={cn(
"flex max-w-sm flex-col items-center gap-2 text-center", "flex max-w-sm flex-col items-center gap-2 text-center",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
const emptyMediaVariants = cva( const emptyMediaVariants = cva(
@@ -34,14 +34,15 @@ const emptyMediaVariants = cva(
variants: { variants: {
variant: { variant: {
default: "bg-transparent", default: "bg-transparent",
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6", icon:
"bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
function EmptyMedia({ function EmptyMedia({
className, className,
@@ -55,7 +56,7 @@ function EmptyMedia({
className={cn(emptyMediaVariants({ variant, className }))} className={cn(emptyMediaVariants({ variant, className }))}
{...props} {...props}
/> />
) );
} }
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -65,7 +66,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
className={cn("text-lg font-medium tracking-tight", className)} className={cn("text-lg font-medium tracking-tight", className)}
{...props} {...props}
/> />
) );
} }
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
@@ -74,11 +75,11 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
data-slot="empty-description" data-slot="empty-description"
className={cn( className={cn(
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4", "text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -87,11 +88,11 @@ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="empty-content" data-slot="empty-content"
className={cn( className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance", "flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -101,4 +102,4 @@ export {
EmptyDescription, EmptyDescription,
EmptyContent, EmptyContent,
EmptyMedia, EmptyMedia,
} };

View File

@@ -1,11 +1,10 @@
"use client" "use client";
import { useMemo } from "react" import { cva, type VariantProps } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority" import { useMemo } from "react";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils" import { Separator } from "@/components/ui/separator";
import { Label } from "@/components/ui/label" import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return ( return (
@@ -14,11 +13,11 @@ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
className={cn( className={cn(
"flex flex-col gap-6", "flex flex-col gap-6",
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function FieldLegend({ function FieldLegend({
@@ -34,11 +33,11 @@ function FieldLegend({
"mb-3 font-medium", "mb-3 font-medium",
"data-[variant=legend]:text-base", "data-[variant=legend]:text-base",
"data-[variant=label]:text-sm", "data-[variant=label]:text-sm",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
@@ -47,11 +46,11 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
data-slot="field-group" data-slot="field-group"
className={cn( className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4", "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
const fieldVariants = cva( const fieldVariants = cva(
@@ -75,8 +74,8 @@ const fieldVariants = cva(
defaultVariants: { defaultVariants: {
orientation: "vertical", orientation: "vertical",
}, },
} },
) );
function Field({ function Field({
className, className,
@@ -91,7 +90,7 @@ function Field({
className={cn(fieldVariants({ orientation }), className)} className={cn(fieldVariants({ orientation }), className)}
{...props} {...props}
/> />
) );
} }
function FieldContent({ className, ...props }: React.ComponentProps<"div">) { function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -100,11 +99,11 @@ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="field-content" data-slot="field-content"
className={cn( className={cn(
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug", "group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function FieldLabel({ function FieldLabel({
@@ -118,11 +117,11 @@ function FieldLabel({
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50", "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4", "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10", "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -131,11 +130,11 @@ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
data-slot="field-label" data-slot="field-label"
className={cn( className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50", "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
@@ -146,11 +145,11 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance", "text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5", "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function FieldSeparator({ function FieldSeparator({
@@ -158,7 +157,7 @@ function FieldSeparator({
className, className,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
children?: React.ReactNode children?: React.ReactNode;
}) { }) {
return ( return (
<div <div
@@ -166,7 +165,7 @@ function FieldSeparator({
data-content={!!children} data-content={!!children}
className={cn( className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className className,
)} )}
{...props} {...props}
> >
@@ -180,7 +179,7 @@ function FieldSeparator({
</span> </span>
)} )}
</div> </div>
) );
} }
function FieldError({ function FieldError({
@@ -189,37 +188,36 @@ function FieldError({
errors, errors,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined> errors?: Array<{ message?: string } | undefined>;
}) { }) {
const content = useMemo(() => { const content = useMemo(() => {
if (children) { if (children) {
return children return children;
} }
if (!errors?.length) { if (!errors?.length) {
return null return null;
} }
const uniqueErrors = [ const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(), ...new Map(errors.map((error) => [error?.message, error])).values(),
] ];
if (uniqueErrors?.length == 1) { if (uniqueErrors?.length === 1) {
return uniqueErrors[0]?.message return uniqueErrors[0]?.message;
} }
return ( return (
<ul className="ml-4 flex list-disc flex-col gap-1"> <ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map( {uniqueErrors.map(
(error, index) => (error) => error?.message && <li key={error.message}>{error.message}</li>,
error?.message && <li key={index}>{error.message}</li>
)} )}
</ul> </ul>
) );
}, [children, errors]) }, [children, errors]);
if (!content) { if (!content) {
return null return null;
} }
return ( return (
@@ -231,7 +229,7 @@ function FieldError({
> >
{content} {content}
</div> </div>
) );
} }
export { export {
@@ -245,4 +243,4 @@ export {
FieldSet, FieldSet,
FieldContent, FieldContent,
FieldTitle, FieldTitle,
} };

View File

@@ -1,33 +1,32 @@
"use client" "use client";
import * as React from "react" import type * as LabelPrimitive from "@radix-ui/react-label";
import type * as LabelPrimitive from "@radix-ui/react-label" import { Slot } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot" import * as React from "react";
import { import {
Controller, Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps, type ControllerProps,
type FieldPath, type FieldPath,
type FieldValues, type FieldValues,
} from "react-hook-form" FormProvider,
useFormContext,
useFormState,
} from "react-hook-form";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { cn } from "@/lib/utils" const Form = FormProvider;
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = { > = {
name: TName name: TName;
} };
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue {} as FormFieldContextValue,
) );
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
@@ -39,21 +38,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}> <FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} /> <Controller {...props} />
</FormFieldContext.Provider> </FormFieldContext.Provider>
) );
} };
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext) const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext() const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name }) const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>") throw new Error("useFormField should be used within <FormField>");
} }
const { id } = itemContext const { id } = itemContext;
return { return {
id, id,
@@ -62,19 +61,19 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`, formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`, formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState,
} };
} };
type FormItemContextValue = { type FormItemContextValue = {
id: string id: string;
} };
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue {} as FormItemContextValue,
) );
function FormItem({ className, ...props }: React.ComponentProps<"div">) { function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId() const id = React.useId();
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
@@ -84,14 +83,14 @@ function FormItem({ className, ...props }: React.ComponentProps<"div">) {
{...props} {...props}
/> />
</FormItemContext.Provider> </FormItemContext.Provider>
) );
} }
function FormLabel({ function FormLabel({
className, className,
...props ...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) { }: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField();
return ( return (
<Label <Label
@@ -101,29 +100,27 @@ function FormLabel({
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
) );
} }
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) { function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return ( return (
<Slot <Slot
data-slot="form-control" data-slot="form-control"
id={formItemId} id={formItemId}
aria-describedby={ aria-describedby={
!error !error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
} }
aria-invalid={!!error} aria-invalid={!!error}
{...props} {...props}
/> />
) );
} }
function FormDescription({ className, ...props }: React.ComponentProps<"p">) { function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField() const { formDescriptionId } = useFormField();
return ( return (
<p <p
@@ -132,15 +129,15 @@ function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) );
} }
function FormMessage({ className, ...props }: React.ComponentProps<"p">) { function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? "") : props.children const body = error ? String(error?.message ?? "") : props.children;
if (!body) { if (!body) {
return null return null;
} }
return ( return (
@@ -152,7 +149,7 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
> >
{body} {body}
</p> </p>
) );
} }
export { export {
@@ -164,4 +161,4 @@ export {
FormDescription, FormDescription,
FormMessage, FormMessage,
FormField, FormField,
} };

View File

@@ -1,14 +1,14 @@
"use client" "use client";
import * as React from "react" import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function HoverCard({ function HoverCard({
...props ...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) { }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} /> return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
} }
function HoverCardTrigger({ function HoverCardTrigger({
@@ -16,7 +16,7 @@ function HoverCardTrigger({
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) { }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return ( return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} /> <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
) );
} }
function HoverCardContent({ function HoverCardContent({
@@ -33,12 +33,12 @@ function HoverCardContent({
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden", "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-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className className,
)} )}
{...props} {...props}
/> />
</HoverCardPrimitive.Portal> </HoverCardPrimitive.Portal>
) );
} }
export { HoverCard, HoverCardTrigger, HoverCardContent } export { HoverCard, HoverCardTrigger, HoverCardContent };

View File

@@ -1,12 +1,11 @@
"use client" "use client";
import * as React from "react" import { cva, type VariantProps } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority" import type * as React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils" import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input" import { cn } from "@/lib/utils";
import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) { function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
@@ -29,11 +28,11 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
// Error state. // Error state.
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40", "has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
const inputGroupAddonVariants = cva( const inputGroupAddonVariants = cva(
@@ -54,8 +53,8 @@ const inputGroupAddonVariants = cva(
defaultVariants: { defaultVariants: {
align: "inline-start", align: "inline-start",
}, },
} },
) );
function InputGroupAddon({ function InputGroupAddon({
className, className,
@@ -70,13 +69,22 @@ function InputGroupAddon({
className={cn(inputGroupAddonVariants({ align }), className)} className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => { onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) { if ((e.target as HTMLElement).closest("button")) {
return return;
}
e.currentTarget.parentElement?.querySelector("input")?.focus();
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if ((e.target as HTMLElement).closest("button")) {
return;
}
e.preventDefault();
e.currentTarget.parentElement?.querySelector("input")?.focus();
} }
e.currentTarget.parentElement?.querySelector("input")?.focus()
}} }}
{...props} {...props}
/> />
) );
} }
const inputGroupButtonVariants = cva( const inputGroupButtonVariants = cva(
@@ -86,16 +94,15 @@ const inputGroupButtonVariants = cva(
size: { size: {
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2", xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5", sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
"icon-xs": "icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0", "icon-sm": "size-8 p-0 has-[>svg]:p-0",
}, },
}, },
defaultVariants: { defaultVariants: {
size: "xs", size: "xs",
}, },
} },
) );
function InputGroupButton({ function InputGroupButton({
className, className,
@@ -113,7 +120,7 @@ function InputGroupButton({
className={cn(inputGroupButtonVariants({ size }), className)} className={cn(inputGroupButtonVariants({ size }), className)}
{...props} {...props}
/> />
) );
} }
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) { function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
@@ -121,11 +128,11 @@ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
<span <span
className={cn( className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", "text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function InputGroupInput({ function InputGroupInput({
@@ -137,11 +144,11 @@ function InputGroupInput({
data-slot="input-group-control" data-slot="input-group-control"
className={cn( className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent", "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function InputGroupTextarea({ function InputGroupTextarea({
@@ -153,11 +160,11 @@ function InputGroupTextarea({
data-slot="input-group-control" data-slot="input-group-control"
className={cn( className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent", "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -167,4 +174,4 @@ export {
InputGroupText, InputGroupText,
InputGroupInput, InputGroupInput,
InputGroupTextarea, InputGroupTextarea,
} };

View File

@@ -1,29 +1,29 @@
"use client" "use client";
import * as React from "react" import { OTPInput, OTPInputContext } from "input-otp";
import { OTPInput, OTPInputContext } from "input-otp" import { MinusIcon } from "lucide-react";
import { MinusIcon } from "lucide-react" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function InputOTP({ function InputOTP({
className, className,
containerClassName, containerClassName,
...props ...props
}: React.ComponentProps<typeof OTPInput> & { }: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string containerClassName?: string;
}) { }) {
return ( return (
<OTPInput <OTPInput
data-slot="input-otp" data-slot="input-otp"
containerClassName={cn( containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50", "flex items-center gap-2 has-disabled:opacity-50",
containerClassName containerClassName,
)} )}
className={cn("disabled:cursor-not-allowed", className)} className={cn("disabled:cursor-not-allowed", className)}
{...props} {...props}
/> />
) );
} }
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) { function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
@@ -33,7 +33,7 @@ function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex items-center", className)} className={cn("flex items-center", className)}
{...props} {...props}
/> />
) );
} }
function InputOTPSlot({ function InputOTPSlot({
@@ -41,10 +41,10 @@ function InputOTPSlot({
className, className,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
index: number index: number;
}) { }) {
const inputOTPContext = React.useContext(OTPInputContext) const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {} const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
return ( return (
<div <div
@@ -52,7 +52,7 @@ function InputOTPSlot({
data-active={isActive} data-active={isActive}
className={cn( className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]", "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className className,
)} )}
{...props} {...props}
> >
@@ -63,7 +63,7 @@ function InputOTPSlot({
</div> </div>
)} )}
</div> </div>
) );
} }
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) { function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
@@ -71,7 +71,7 @@ function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
<div data-slot="input-otp-separator" role="separator" {...props}> <div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon /> <MinusIcon />
</div> </div>
) );
} }
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

View File

@@ -1,6 +1,6 @@
import * as React from "react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) { function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return ( return (
@@ -11,11 +11,11 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input 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", "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input 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]", "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",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Input } export { Input };

View File

@@ -1,9 +1,8 @@
import * as React from "react" import { Slot } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority" import type * as React from "react";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator"
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) { function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
@@ -13,7 +12,7 @@ function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
className={cn("group/item-group flex flex-col", className)} className={cn("group/item-group flex flex-col", className)}
{...props} {...props}
/> />
) );
} }
function ItemSeparator({ function ItemSeparator({
@@ -27,7 +26,7 @@ function ItemSeparator({
className={cn("my-0", className)} className={cn("my-0", className)}
{...props} {...props}
/> />
) );
} }
const itemVariants = cva( const itemVariants = cva(
@@ -48,8 +47,8 @@ const itemVariants = cva(
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) );
function Item({ function Item({
className, className,
@@ -59,7 +58,7 @@ function Item({
...props ...props
}: React.ComponentProps<"div"> & }: React.ComponentProps<"div"> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) { VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div" const Comp = asChild ? Slot : "div";
return ( return (
<Comp <Comp
data-slot="item" data-slot="item"
@@ -68,7 +67,7 @@ function Item({
className={cn(itemVariants({ variant, size, className }))} className={cn(itemVariants({ variant, size, className }))}
{...props} {...props}
/> />
) );
} }
const itemMediaVariants = cva( const itemMediaVariants = cva(
@@ -77,7 +76,8 @@ const itemMediaVariants = cva(
variants: { variants: {
variant: { variant: {
default: "bg-transparent", default: "bg-transparent",
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4", icon:
"size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image: image:
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover", "size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
}, },
@@ -85,8 +85,8 @@ const itemMediaVariants = cva(
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
} },
) );
function ItemMedia({ function ItemMedia({
className, className,
@@ -100,7 +100,7 @@ function ItemMedia({
className={cn(itemMediaVariants({ variant, className }))} className={cn(itemMediaVariants({ variant, className }))}
{...props} {...props}
/> />
) );
} }
function ItemContent({ className, ...props }: React.ComponentProps<"div">) { function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -109,11 +109,11 @@ function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
data-slot="item-content" data-slot="item-content"
className={cn( className={cn(
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none", "flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) { function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
@@ -122,11 +122,11 @@ function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
data-slot="item-title" data-slot="item-title"
className={cn( className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium", "flex w-fit items-center gap-2 text-sm leading-snug font-medium",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) { function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
@@ -136,11 +136,11 @@ function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
className={cn( className={cn(
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance", "text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ItemActions({ className, ...props }: React.ComponentProps<"div">) { function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
@@ -150,7 +150,7 @@ function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex items-center gap-2", className)} className={cn("flex items-center gap-2", className)}
{...props} {...props}
/> />
) );
} }
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) { function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -159,11 +159,11 @@ function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
data-slot="item-header" data-slot="item-header"
className={cn( className={cn(
"flex basis-full items-center justify-between gap-2", "flex basis-full items-center justify-between gap-2",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) { function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -172,11 +172,11 @@ function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
data-slot="item-footer" data-slot="item-footer"
className={cn( className={cn(
"flex basis-full items-center justify-between gap-2", "flex basis-full items-center justify-between gap-2",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -190,4 +190,4 @@ export {
ItemDescription, ItemDescription,
ItemHeader, ItemHeader,
ItemFooter, ItemFooter,
} };

View File

@@ -1,4 +1,4 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) { function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
return ( return (
@@ -8,11 +8,11 @@ function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none", "bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
"[&_svg:not([class*='size-'])]:size-3", "[&_svg:not([class*='size-'])]:size-3",
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10", "[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) { function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
@@ -22,7 +22,7 @@ function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
className={cn("inline-flex items-center gap-1", className)} className={cn("inline-flex items-center gap-1", className)}
{...props} {...props}
/> />
) );
} }
export { Kbd, KbdGroup } export { Kbd, KbdGroup };

View File

@@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as LabelPrimitive from "@radix-ui/react-label";
import * as LabelPrimitive from "@radix-ui/react-label" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Label({ function Label({
className, className,
@@ -14,11 +14,11 @@ function Label({
data-slot="label" data-slot="label"
className={cn( className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Label } export { Label };

View File

@@ -1,10 +1,10 @@
"use client" "use client";
import * as React from "react" import * as MenubarPrimitive from "@radix-ui/react-menubar";
import * as MenubarPrimitive from "@radix-ui/react-menubar" import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Menubar({ function Menubar({
className, className,
@@ -15,29 +15,29 @@ function Menubar({
data-slot="menubar" data-slot="menubar"
className={cn( className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs", "bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function MenubarMenu({ function MenubarMenu({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) { }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} /> return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
} }
function MenubarGroup({ function MenubarGroup({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) { }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} /> return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
} }
function MenubarPortal({ function MenubarPortal({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) { }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} /> return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
} }
function MenubarRadioGroup({ function MenubarRadioGroup({
@@ -45,7 +45,7 @@ function MenubarRadioGroup({
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) { }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return ( return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} /> <MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
) );
} }
function MenubarTrigger({ function MenubarTrigger({
@@ -57,11 +57,11 @@ function MenubarTrigger({
data-slot="menubar-trigger" data-slot="menubar-trigger"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none", "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function MenubarContent({ function MenubarContent({
@@ -80,12 +80,12 @@ function MenubarContent({
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in 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 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md", "bg-popover text-popover-foreground data-[state=open]:animate-in 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 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className className,
)} )}
{...props} {...props}
/> />
</MenubarPortal> </MenubarPortal>
) );
} }
function MenubarItem({ function MenubarItem({
@@ -94,8 +94,8 @@ function MenubarItem({
variant = "default", variant = "default",
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & { }: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean inset?: boolean;
variant?: "default" | "destructive" variant?: "default" | "destructive";
}) { }) {
return ( return (
<MenubarPrimitive.Item <MenubarPrimitive.Item
@@ -104,11 +104,11 @@ function MenubarItem({
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function MenubarCheckboxItem({ function MenubarCheckboxItem({
@@ -122,7 +122,7 @@ function MenubarCheckboxItem({
data-slot="menubar-checkbox-item" data-slot="menubar-checkbox-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
checked={checked} checked={checked}
{...props} {...props}
@@ -134,7 +134,7 @@ function MenubarCheckboxItem({
</span> </span>
{children} {children}
</MenubarPrimitive.CheckboxItem> </MenubarPrimitive.CheckboxItem>
) );
} }
function MenubarRadioItem({ function MenubarRadioItem({
@@ -147,7 +147,7 @@ function MenubarRadioItem({
data-slot="menubar-radio-item" data-slot="menubar-radio-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
> >
@@ -158,7 +158,7 @@ function MenubarRadioItem({
</span> </span>
{children} {children}
</MenubarPrimitive.RadioItem> </MenubarPrimitive.RadioItem>
) );
} }
function MenubarLabel({ function MenubarLabel({
@@ -166,7 +166,7 @@ function MenubarLabel({
inset, inset,
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & { }: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<MenubarPrimitive.Label <MenubarPrimitive.Label
@@ -174,11 +174,11 @@ function MenubarLabel({
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function MenubarSeparator({ function MenubarSeparator({
@@ -191,7 +191,7 @@ function MenubarSeparator({
className={cn("bg-border -mx-1 my-1 h-px", className)} className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props} {...props}
/> />
) );
} }
function MenubarShortcut({ function MenubarShortcut({
@@ -203,17 +203,17 @@ function MenubarShortcut({
data-slot="menubar-shortcut" data-slot="menubar-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "text-muted-foreground ml-auto text-xs tracking-widest",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function MenubarSub({ function MenubarSub({
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) { }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} /> return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
} }
function MenubarSubTrigger({ function MenubarSubTrigger({
@@ -222,7 +222,7 @@ function MenubarSubTrigger({
children, children,
...props ...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & { }: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean;
}) { }) {
return ( return (
<MenubarPrimitive.SubTrigger <MenubarPrimitive.SubTrigger
@@ -230,14 +230,14 @@ function MenubarSubTrigger({
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8", "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto h-4 w-4" /> <ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger> </MenubarPrimitive.SubTrigger>
) );
} }
function MenubarSubContent({ function MenubarSubContent({
@@ -249,11 +249,11 @@ function MenubarSubContent({
data-slot="menubar-sub-content" data-slot="menubar-sub-content"
className={cn( 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 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", "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 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -273,4 +273,4 @@ export {
MenubarSub, MenubarSub,
MenubarSubTrigger, MenubarSubTrigger,
MenubarSubContent, MenubarSubContent,
} };

View File

@@ -1,9 +1,9 @@
import * as React from "react" import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" import { cva } from "class-variance-authority";
import { cva } from "class-variance-authority" import { ChevronDownIcon } from "lucide-react";
import { ChevronDownIcon } from "lucide-react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function NavigationMenu({ function NavigationMenu({
className, className,
@@ -11,7 +11,7 @@ function NavigationMenu({
viewport = true, viewport = true,
...props ...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & { }: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean viewport?: boolean;
}) { }) {
return ( return (
<NavigationMenuPrimitive.Root <NavigationMenuPrimitive.Root
@@ -19,14 +19,14 @@ function NavigationMenu({
data-viewport={viewport} data-viewport={viewport}
className={cn( className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center", "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
{viewport && <NavigationMenuViewport />} {viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root> </NavigationMenuPrimitive.Root>
) );
} }
function NavigationMenuList({ function NavigationMenuList({
@@ -38,11 +38,11 @@ function NavigationMenuList({
data-slot="navigation-menu-list" data-slot="navigation-menu-list"
className={cn( className={cn(
"group flex flex-1 list-none items-center justify-center gap-1", "group flex flex-1 list-none items-center justify-center gap-1",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function NavigationMenuItem({ function NavigationMenuItem({
@@ -55,12 +55,12 @@ function NavigationMenuItem({
className={cn("relative", className)} className={cn("relative", className)}
{...props} {...props}
/> />
) );
} }
const navigationMenuTriggerStyle = cva( const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1" "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
) );
function NavigationMenuTrigger({ function NavigationMenuTrigger({
className, className,
@@ -79,7 +79,7 @@ function NavigationMenuTrigger({
aria-hidden="true" aria-hidden="true"
/> />
</NavigationMenuPrimitive.Trigger> </NavigationMenuPrimitive.Trigger>
) );
} }
function NavigationMenuContent({ function NavigationMenuContent({
@@ -92,11 +92,11 @@ function NavigationMenuContent({
className={cn( className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto", "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none", "group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function NavigationMenuViewport({ function NavigationMenuViewport({
@@ -105,20 +105,18 @@ function NavigationMenuViewport({
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) { }: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return ( return (
<div <div
className={cn( className={cn("absolute top-full left-0 isolate z-50 flex justify-center")}
"absolute top-full left-0 isolate z-50 flex justify-center"
)}
> >
<NavigationMenuPrimitive.Viewport <NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport" data-slot="navigation-menu-viewport"
className={cn( className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]", "origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className className,
)} )}
{...props} {...props}
/> />
</div> </div>
) );
} }
function NavigationMenuLink({ function NavigationMenuLink({
@@ -130,11 +128,11 @@ function NavigationMenuLink({
data-slot="navigation-menu-link" data-slot="navigation-menu-link"
className={cn( className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4", "data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function NavigationMenuIndicator({ function NavigationMenuIndicator({
@@ -146,13 +144,13 @@ function NavigationMenuIndicator({
data-slot="navigation-menu-indicator" data-slot="navigation-menu-indicator"
className={cn( className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden", "data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className className,
)} )}
{...props} {...props}
> >
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" /> <div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator> </NavigationMenuPrimitive.Indicator>
) );
} }
export { export {
@@ -165,4 +163,4 @@ export {
NavigationMenuIndicator, NavigationMenuIndicator,
NavigationMenuViewport, NavigationMenuViewport,
navigationMenuTriggerStyle, navigationMenuTriggerStyle,
} };

View File

@@ -1,23 +1,21 @@
import * as React from "react"
import { import {
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
MoreHorizontalIcon, MoreHorizontalIcon,
} from "lucide-react" } from "lucide-react";
import type * as React from "react";
import { cn } from "@/lib/utils" import { type Button, buttonVariants } from "@/components/ui/button";
import { buttonVariants, type Button } from "@/components/ui/button" import { cn } from "@/lib/utils";
function Pagination({ className, ...props }: React.ComponentProps<"nav">) { function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return ( return (
<nav <nav
role="navigation"
aria-label="pagination" aria-label="pagination"
data-slot="pagination" data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)} className={cn("mx-auto flex w-full justify-center", className)}
{...props} {...props}
/> />
) );
} }
function PaginationContent({ function PaginationContent({
@@ -30,17 +28,17 @@ function PaginationContent({
className={cn("flex flex-row items-center gap-1", className)} className={cn("flex flex-row items-center gap-1", className)}
{...props} {...props}
/> />
) );
} }
function PaginationItem({ ...props }: React.ComponentProps<"li">) { function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} /> return <li data-slot="pagination-item" {...props} />;
} }
type PaginationLinkProps = { type PaginationLinkProps = {
isActive?: boolean isActive?: boolean;
} & Pick<React.ComponentProps<typeof Button>, "size"> & } & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a"> React.ComponentProps<"a">;
function PaginationLink({ function PaginationLink({
className, className,
@@ -58,11 +56,11 @@ function PaginationLink({
variant: isActive ? "outline" : "ghost", variant: isActive ? "outline" : "ghost",
size, size,
}), }),
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function PaginationPrevious({ function PaginationPrevious({
@@ -79,7 +77,7 @@ function PaginationPrevious({
<ChevronLeftIcon /> <ChevronLeftIcon />
<span className="hidden sm:block">Previous</span> <span className="hidden sm:block">Previous</span>
</PaginationLink> </PaginationLink>
) );
} }
function PaginationNext({ function PaginationNext({
@@ -96,7 +94,7 @@ function PaginationNext({
<span className="hidden sm:block">Next</span> <span className="hidden sm:block">Next</span>
<ChevronRightIcon /> <ChevronRightIcon />
</PaginationLink> </PaginationLink>
) );
} }
function PaginationEllipsis({ function PaginationEllipsis({
@@ -113,7 +111,7 @@ function PaginationEllipsis({
<MoreHorizontalIcon className="size-4" /> <MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span> <span className="sr-only">More pages</span>
</span> </span>
) );
} }
export { export {
@@ -124,4 +122,4 @@ export {
PaginationPrevious, PaginationPrevious,
PaginationNext, PaginationNext,
PaginationEllipsis, PaginationEllipsis,
} };

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 PopoverPrimitive from "@radix-ui/react-popover" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Popover({ function Popover({
...props ...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) { }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} /> return <PopoverPrimitive.Root data-slot="popover" {...props} />;
} }
function PopoverTrigger({ function PopoverTrigger({
...props ...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) { }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} /> return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
} }
function PopoverContent({ function PopoverContent({
@@ -31,18 +31,18 @@ function PopoverContent({
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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 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",
className className,
)} )}
{...props} {...props}
/> />
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
) );
} }
function PopoverAnchor({ function PopoverAnchor({
...props ...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) { }: 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 ProgressPrimitive from "@radix-ui/react-progress";
import * as ProgressPrimitive from "@radix-ui/react-progress" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Progress({ function Progress({
className, className,
@@ -15,7 +15,7 @@ function Progress({
data-slot="progress" data-slot="progress"
className={cn( className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className className,
)} )}
{...props} {...props}
> >
@@ -25,7 +25,7 @@ function Progress({
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>
) );
} }
export { Progress } export { Progress };

View File

@@ -1,10 +1,10 @@
"use client" "use client";
import * as React from "react" import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" import { CircleIcon } from "lucide-react";
import { CircleIcon } from "lucide-react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function RadioGroup({ function RadioGroup({
className, className,
@@ -16,7 +16,7 @@ function RadioGroup({
className={cn("grid gap-3", className)} className={cn("grid gap-3", className)}
{...props} {...props}
/> />
) );
} }
function RadioGroupItem({ function RadioGroupItem({
@@ -28,7 +28,7 @@ function RadioGroupItem({
data-slot="radio-group-item" data-slot="radio-group-item"
className={cn( className={cn(
"border-input text-primary 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 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", "border-input text-primary 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 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >
@@ -39,7 +39,7 @@ function RadioGroupItem({
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" /> <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator> </RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item> </RadioGroupPrimitive.Item>
) );
} }
export { RadioGroup, RadioGroupItem } export { RadioGroup, RadioGroupItem };

View File

@@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function ScrollArea({ function ScrollArea({
className, className,
@@ -25,7 +25,7 @@ function ScrollArea({
<ScrollBar /> <ScrollBar />
<ScrollAreaPrimitive.Corner /> <ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root> </ScrollAreaPrimitive.Root>
) );
} }
function ScrollBar({ function ScrollBar({
@@ -39,11 +39,10 @@ function ScrollBar({
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"flex touch-none p-px transition-colors select-none", "flex touch-none p-px transition-colors select-none",
orientation === "vertical" && orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent",
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" && orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent", "h-2.5 flex-col border-t border-t-transparent",
className className,
)} )}
{...props} {...props}
> >
@@ -52,7 +51,7 @@ function ScrollBar({
className="bg-border relative flex-1 rounded-full" className="bg-border relative flex-1 rounded-full"
/> />
</ScrollAreaPrimitive.ScrollAreaScrollbar> </ScrollAreaPrimitive.ScrollAreaScrollbar>
) );
} }
export { ScrollArea, ScrollBar } export { ScrollArea, ScrollBar };

View File

@@ -1,27 +1,27 @@
"use client" "use client";
import * as React from "react" import * as SelectPrimitive from "@radix-ui/react-select";
import * as SelectPrimitive from "@radix-ui/react-select" import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Select({ function Select({
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) { }: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} /> return <SelectPrimitive.Root data-slot="select" {...props} />;
} }
function SelectGroup({ function SelectGroup({
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) { }: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} /> return <SelectPrimitive.Group data-slot="select-group" {...props} />;
} }
function SelectValue({ function SelectValue({
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) { }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} /> return <SelectPrimitive.Value data-slot="select-value" {...props} />;
} }
function SelectTrigger({ function SelectTrigger({
@@ -30,7 +30,7 @@ function SelectTrigger({
children, children,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & { }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default" size?: "sm" | "default";
}) { }) {
return ( return (
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
@@ -38,7 +38,7 @@ function SelectTrigger({
data-size={size} data-size={size}
className={cn( className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]: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 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]: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 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
> >
@@ -47,7 +47,7 @@ function SelectTrigger({
<ChevronDownIcon className="size-4 opacity-50" /> <ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
) );
} }
function SelectContent({ function SelectContent({
@@ -65,7 +65,7 @@ function SelectContent({
"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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md", "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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" && position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className className,
)} )}
position={position} position={position}
align={align} align={align}
@@ -76,7 +76,7 @@ function SelectContent({
className={cn( className={cn(
"p-1", "p-1",
position === "popper" && position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1" "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)} )}
> >
{children} {children}
@@ -84,7 +84,7 @@ function SelectContent({
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPrimitive.Portal>
) );
} }
function SelectLabel({ function SelectLabel({
@@ -97,7 +97,7 @@ function SelectLabel({
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)} className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props} {...props}
/> />
) );
} }
function SelectItem({ function SelectItem({
@@ -110,7 +110,7 @@ function SelectItem({
data-slot="select-item" data-slot="select-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className className,
)} )}
{...props} {...props}
> >
@@ -124,7 +124,7 @@ function SelectItem({
</span> </span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item> </SelectPrimitive.Item>
) );
} }
function SelectSeparator({ function SelectSeparator({
@@ -137,7 +137,7 @@ function SelectSeparator({
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)} className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props} {...props}
/> />
) );
} }
function SelectScrollUpButton({ function SelectScrollUpButton({
@@ -149,13 +149,13 @@ function SelectScrollUpButton({
data-slot="select-scroll-up-button" data-slot="select-scroll-up-button"
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", "flex cursor-default items-center justify-center py-1",
className className,
)} )}
{...props} {...props}
> >
<ChevronUpIcon className="size-4" /> <ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpButton>
) );
} }
function SelectScrollDownButton({ function SelectScrollDownButton({
@@ -167,13 +167,13 @@ function SelectScrollDownButton({
data-slot="select-scroll-down-button" data-slot="select-scroll-down-button"
className={cn( className={cn(
"flex cursor-default items-center justify-center py-1", "flex cursor-default items-center justify-center py-1",
className className,
)} )}
{...props} {...props}
> >
<ChevronDownIcon className="size-4" /> <ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
) );
} }
export { export {
@@ -187,4 +187,4 @@ export {
SelectSeparator, SelectSeparator,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} };

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 SeparatorPrimitive from "@radix-ui/react-separator" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Separator({ function Separator({
className, className,
@@ -18,11 +18,11 @@ function Separator({
orientation={orientation} orientation={orientation}
className={cn( 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 shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Separator } export { Separator };

View File

@@ -1,31 +1,31 @@
"use client" "use client";
import * as React from "react" import * as SheetPrimitive from "@radix-ui/react-dialog";
import * as SheetPrimitive from "@radix-ui/react-dialog" import { XIcon } from "lucide-react";
import { XIcon } from "lucide-react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} /> return <SheetPrimitive.Root data-slot="sheet" {...props} />;
} }
function SheetTrigger({ function SheetTrigger({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) { }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
} }
function SheetClose({ function SheetClose({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) { }: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
} }
function SheetPortal({ function SheetPortal({
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) { }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
} }
function SheetOverlay({ function SheetOverlay({
@@ -37,11 +37,11 @@ function SheetOverlay({
data-slot="sheet-overlay" data-slot="sheet-overlay"
className={cn( className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function SheetContent({ function SheetContent({
@@ -50,7 +50,7 @@ function SheetContent({
side = "right", side = "right",
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & { }: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left" side?: "top" | "right" | "bottom" | "left";
}) { }) {
return ( return (
<SheetPortal> <SheetPortal>
@@ -67,7 +67,7 @@ function SheetContent({
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" && side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className className,
)} )}
{...props} {...props}
> >
@@ -78,7 +78,7 @@ function SheetContent({
</SheetPrimitive.Close> </SheetPrimitive.Close>
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPortal>
) );
} }
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -88,7 +88,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-1.5 p-4", className)} className={cn("flex flex-col gap-1.5 p-4", className)}
{...props} {...props}
/> />
) );
} }
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -98,7 +98,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("mt-auto flex flex-col gap-2 p-4", className)} className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props} {...props}
/> />
) );
} }
function SheetTitle({ function SheetTitle({
@@ -111,7 +111,7 @@ function SheetTitle({
className={cn("text-foreground font-semibold", className)} className={cn("text-foreground font-semibold", className)}
{...props} {...props}
/> />
) );
} }
function SheetDescription({ function SheetDescription({
@@ -124,7 +124,7 @@ function SheetDescription({
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -136,4 +136,4 @@ export {
SheetFooter, SheetFooter,
SheetTitle, SheetTitle,
SheetDescription, SheetDescription,
} };

View File

@@ -1,56 +1,55 @@
"use client" "use client";
import * as React from "react" import { Slot } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority" import { PanelLeftIcon } from "lucide-react";
import { PanelLeftIcon } from "lucide-react" import * as React from "react";
import { Button } from "@/components/ui/button";
import { useIsMobile } from "@/hooks/use-mobile" import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils" import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
SheetDescription, SheetDescription,
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
} from "@/components/ui/sheet" } from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip" } from "@/components/ui/tooltip";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
const SIDEBAR_COOKIE_NAME = "sidebar_state" const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem" const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem" const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem" const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b" const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = { type SidebarContextProps = {
state: "expanded" | "collapsed" state: "expanded" | "collapsed";
open: boolean open: boolean;
setOpen: (open: boolean) => void setOpen: (open: boolean) => void;
openMobile: boolean openMobile: boolean;
setOpenMobile: (open: boolean) => void setOpenMobile: (open: boolean) => void;
isMobile: boolean isMobile: boolean;
toggleSidebar: () => void toggleSidebar: () => void;
} };
const SidebarContext = React.createContext<SidebarContextProps | null>(null) const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() { function useSidebar() {
const context = React.useContext(SidebarContext) const context = React.useContext(SidebarContext);
if (!context) { if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.") throw new Error("useSidebar must be used within a SidebarProvider.");
} }
return context return context;
} }
function SidebarProvider({ function SidebarProvider({
@@ -62,36 +61,36 @@ function SidebarProvider({
children, children,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
defaultOpen?: boolean defaultOpen?: boolean;
open?: boolean open?: boolean;
onOpenChange?: (open: boolean) => void onOpenChange?: (open: boolean) => void;
}) { }) {
const isMobile = useIsMobile() const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false) const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar. // This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component. // We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen) const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open const open = openProp ?? _open;
const setOpen = React.useCallback( const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => { (value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) { if (setOpenProp) {
setOpenProp(openState) setOpenProp(openState);
} else { } else {
_setOpen(openState) _setOpen(openState);
} }
// This sets the cookie to keep the sidebar state. // This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
}, },
[setOpenProp, open] [setOpenProp, open],
) );
// Helper to toggle the sidebar. // Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => { const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]) }, [isMobile, setOpen]);
// Adds a keyboard shortcut to toggle the sidebar. // Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => { React.useEffect(() => {
@@ -100,18 +99,18 @@ function SidebarProvider({
event.key === SIDEBAR_KEYBOARD_SHORTCUT && event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey) (event.metaKey || event.ctrlKey)
) { ) {
event.preventDefault() event.preventDefault();
toggleSidebar() toggleSidebar();
}
} }
};
window.addEventListener("keydown", handleKeyDown) window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown) return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]) }, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed". // We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes. // This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed" const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContextProps>( const contextValue = React.useMemo<SidebarContextProps>(
() => ({ () => ({
@@ -123,8 +122,8 @@ function SidebarProvider({
setOpenMobile, setOpenMobile,
toggleSidebar, toggleSidebar,
}), }),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] [state, open, setOpen, isMobile, openMobile, toggleSidebar],
) );
return ( return (
<SidebarContext.Provider value={contextValue}> <SidebarContext.Provider value={contextValue}>
@@ -140,7 +139,7 @@ function SidebarProvider({
} }
className={cn( className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className className,
)} )}
{...props} {...props}
> >
@@ -148,7 +147,7 @@ function SidebarProvider({
</div> </div>
</TooltipProvider> </TooltipProvider>
</SidebarContext.Provider> </SidebarContext.Provider>
) );
} }
function Sidebar({ function Sidebar({
@@ -159,11 +158,11 @@ function Sidebar({
children, children,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
side?: "left" | "right" side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset" variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none" collapsible?: "offcanvas" | "icon" | "none";
}) { }) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar() const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") { if (collapsible === "none") {
return ( return (
@@ -171,13 +170,13 @@ function Sidebar({
data-slot="sidebar" data-slot="sidebar"
className={cn( className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
</div> </div>
) );
} }
if (isMobile) { if (isMobile) {
@@ -202,7 +201,7 @@ function Sidebar({
<div className="flex h-full w-full flex-col">{children}</div> <div className="flex h-full w-full flex-col">{children}</div>
</SheetContent> </SheetContent>
</Sheet> </Sheet>
) );
} }
return ( return (
@@ -223,7 +222,7 @@ function Sidebar({
"group-data-[side=right]:rotate-180", "group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset" variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)" : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)} )}
/> />
<div <div
@@ -237,7 +236,7 @@ function Sidebar({
variant === "floating" || variant === "inset" variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className className,
)} )}
{...props} {...props}
> >
@@ -250,7 +249,7 @@ function Sidebar({
</div> </div>
</div> </div>
</div> </div>
) );
} }
function SidebarTrigger({ function SidebarTrigger({
@@ -258,7 +257,7 @@ function SidebarTrigger({
onClick, onClick,
...props ...props
}: React.ComponentProps<typeof Button>) { }: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar() const { toggleSidebar } = useSidebar();
return ( return (
<Button <Button
@@ -268,19 +267,19 @@ function SidebarTrigger({
size="icon" size="icon"
className={cn("size-7", className)} className={cn("size-7", className)}
onClick={(event) => { onClick={(event) => {
onClick?.(event) onClick?.(event);
toggleSidebar() toggleSidebar();
}} }}
{...props} {...props}
> >
<PanelLeftIcon /> <PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span> <span className="sr-only">Toggle Sidebar</span>
</Button> </Button>
) );
} }
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar() const { toggleSidebar } = useSidebar();
return ( return (
<button <button
@@ -297,11 +296,11 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full", "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
@@ -311,11 +310,11 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
className={cn( className={cn(
"bg-background relative flex w-full flex-1 flex-col", "bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function SidebarInput({ function SidebarInput({
@@ -329,7 +328,7 @@ function SidebarInput({
className={cn("bg-background h-8 w-full shadow-none", className)} className={cn("bg-background h-8 w-full shadow-none", className)}
{...props} {...props}
/> />
) );
} }
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -340,7 +339,7 @@ function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 p-2", className)} className={cn("flex flex-col gap-2 p-2", className)}
{...props} {...props}
/> />
) );
} }
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -351,7 +350,7 @@ function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 p-2", className)} className={cn("flex flex-col gap-2 p-2", className)}
{...props} {...props}
/> />
) );
} }
function SidebarSeparator({ function SidebarSeparator({
@@ -365,7 +364,7 @@ function SidebarSeparator({
className={cn("bg-sidebar-border mx-2 w-auto", className)} className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props} {...props}
/> />
) );
} }
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -375,11 +374,11 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
data-sidebar="content" data-sidebar="content"
className={cn( className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
@@ -390,7 +389,7 @@ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
className={cn("relative flex w-full min-w-0 flex-col p-2", className)} className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props} {...props}
/> />
) );
} }
function SidebarGroupLabel({ function SidebarGroupLabel({
@@ -398,7 +397,7 @@ function SidebarGroupLabel({
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) { }: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div" const Comp = asChild ? Slot : "div";
return ( return (
<Comp <Comp
@@ -407,11 +406,11 @@ function SidebarGroupLabel({
className={cn( className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function SidebarGroupAction({ function SidebarGroupAction({
@@ -419,7 +418,7 @@ function SidebarGroupAction({
asChild = false, asChild = false,
...props ...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) { }: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
@@ -430,11 +429,11 @@ function SidebarGroupAction({
// Increases the hit area of the button on mobile. // Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden", "after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function SidebarGroupContent({ function SidebarGroupContent({
@@ -448,7 +447,7 @@ function SidebarGroupContent({
className={cn("w-full text-sm", className)} className={cn("w-full text-sm", className)}
{...props} {...props}
/> />
) );
} }
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
@@ -459,7 +458,7 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
className={cn("flex w-full min-w-0 flex-col gap-1", className)} className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props} {...props}
/> />
) );
} }
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
@@ -470,7 +469,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
className={cn("group/menu-item relative", className)} className={cn("group/menu-item relative", className)}
{...props} {...props}
/> />
) );
} }
const sidebarMenuButtonVariants = cva( const sidebarMenuButtonVariants = cva(
@@ -492,8 +491,8 @@ const sidebarMenuButtonVariants = cva(
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) );
function SidebarMenuButton({ function SidebarMenuButton({
asChild = false, asChild = false,
@@ -504,12 +503,12 @@ function SidebarMenuButton({
className, className,
...props ...props
}: React.ComponentProps<"button"> & { }: React.ComponentProps<"button"> & {
asChild?: boolean asChild?: boolean;
isActive?: boolean isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent> tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) { } & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar() const { isMobile, state } = useSidebar();
const button = ( const button = (
<Comp <Comp
@@ -520,16 +519,16 @@ function SidebarMenuButton({
className={cn(sidebarMenuButtonVariants({ variant, size }), className)} className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props} {...props}
/> />
) );
if (!tooltip) { if (!tooltip) {
return button return button;
} }
if (typeof tooltip === "string") { if (typeof tooltip === "string") {
tooltip = { tooltip = {
children: tooltip, children: tooltip,
} };
} }
return ( return (
@@ -542,7 +541,7 @@ function SidebarMenuButton({
{...tooltip} {...tooltip}
/> />
</Tooltip> </Tooltip>
) );
} }
function SidebarMenuAction({ function SidebarMenuAction({
@@ -551,10 +550,10 @@ function SidebarMenuAction({
showOnHover = false, showOnHover = false,
...props ...props
}: React.ComponentProps<"button"> & { }: React.ComponentProps<"button"> & {
asChild?: boolean asChild?: boolean;
showOnHover?: boolean showOnHover?: boolean;
}) { }) {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button";
return ( return (
<Comp <Comp
@@ -570,11 +569,11 @@ function SidebarMenuAction({
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
showOnHover && showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function SidebarMenuBadge({ function SidebarMenuBadge({
@@ -592,11 +591,11 @@ function SidebarMenuBadge({
"peer-data-[size=default]/menu-button:top-1.5", "peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5", "peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function SidebarMenuSkeleton({ function SidebarMenuSkeleton({
@@ -604,12 +603,12 @@ function SidebarMenuSkeleton({
showIcon = false, showIcon = false,
...props ...props
}: React.ComponentProps<"div"> & { }: React.ComponentProps<"div"> & {
showIcon?: boolean showIcon?: boolean;
}) { }) {
// Random width between 50 to 90%. // Random width between 50 to 90%.
const width = React.useMemo(() => { const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%` return `${Math.floor(Math.random() * 40) + 50}%`;
}, []) }, []);
return ( return (
<div <div
@@ -619,10 +618,7 @@ function SidebarMenuSkeleton({
{...props} {...props}
> >
{showIcon && ( {showIcon && (
<Skeleton <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)} )}
<Skeleton <Skeleton
className="h-4 max-w-(--skeleton-width) flex-1" className="h-4 max-w-(--skeleton-width) flex-1"
@@ -634,7 +630,7 @@ function SidebarMenuSkeleton({
} }
/> />
</div> </div>
) );
} }
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
@@ -645,11 +641,11 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
className={cn( className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5", "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function SidebarMenuSubItem({ function SidebarMenuSubItem({
@@ -663,7 +659,7 @@ function SidebarMenuSubItem({
className={cn("group/menu-sub-item relative", className)} className={cn("group/menu-sub-item relative", className)}
{...props} {...props}
/> />
) );
} }
function SidebarMenuSubButton({ function SidebarMenuSubButton({
@@ -673,11 +669,11 @@ function SidebarMenuSubButton({
className, className,
...props ...props
}: React.ComponentProps<"a"> & { }: React.ComponentProps<"a"> & {
asChild?: boolean asChild?: boolean;
size?: "sm" | "md" size?: "sm" | "md";
isActive?: boolean isActive?: boolean;
}) { }) {
const Comp = asChild ? Slot : "a" const Comp = asChild ? Slot : "a";
return ( return (
<Comp <Comp
@@ -691,11 +687,11 @@ function SidebarMenuSubButton({
size === "sm" && "text-xs", size === "sm" && "text-xs",
size === "md" && "text-sm", size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden", "group-data-[collapsible=icon]:hidden",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -723,4 +719,4 @@ export {
SidebarSeparator, SidebarSeparator,
SidebarTrigger, SidebarTrigger,
useSidebar, useSidebar,
} };

View File

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

View File

@@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as SliderPrimitive from "@radix-ui/react-slider";
import * as SliderPrimitive from "@radix-ui/react-slider" import * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Slider({ function Slider({
className, className,
@@ -20,8 +20,8 @@ function Slider({
: Array.isArray(defaultValue) : Array.isArray(defaultValue)
? defaultValue ? defaultValue
: [min, max], : [min, max],
[value, defaultValue, min, max] [value, defaultValue, min, max],
) );
return ( return (
<SliderPrimitive.Root <SliderPrimitive.Root
@@ -32,32 +32,32 @@ function Slider({
max={max} max={max}
className={cn( className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className className,
)} )}
{...props} {...props}
> >
<SliderPrimitive.Track <SliderPrimitive.Track
data-slot="slider-track" data-slot="slider-track"
className={cn( className={cn(
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5" "bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
)} )}
> >
<SliderPrimitive.Range <SliderPrimitive.Range
data-slot="slider-range" data-slot="slider-range"
className={cn( className={cn(
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full" "bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
)} )}
/> />
</SliderPrimitive.Track> </SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => ( {_values.map((val, index) => (
<SliderPrimitive.Thumb <SliderPrimitive.Thumb
data-slot="slider-thumb" data-slot="slider-thumb"
key={index} key={`thumb-${index}-${val}`}
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/> />
))} ))}
</SliderPrimitive.Root> </SliderPrimitive.Root>
) );
} }
export { Slider } export { Slider };

View File

@@ -1,4 +1,4 @@
"use client" "use client";
import { import {
CircleCheckIcon, CircleCheckIcon,
@@ -6,12 +6,12 @@ import {
Loader2Icon, Loader2Icon,
OctagonXIcon, OctagonXIcon,
TriangleAlertIcon, TriangleAlertIcon,
} from "lucide-react" } from "lucide-react";
import { useTheme } from "next-themes" import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner" import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme() const { theme = "system" } = useTheme();
return ( return (
<Sonner <Sonner
@@ -34,7 +34,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
} }
{...props} {...props}
/> />
) );
} };
export { Toaster } export { Toaster };

View File

@@ -1,6 +1,6 @@
import { Loader2Icon } from "lucide-react" import { Loader2Icon } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Spinner({ className, ...props }: React.ComponentProps<"svg">) { function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return ( return (
@@ -10,7 +10,7 @@ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
className={cn("size-4 animate-spin", className)} className={cn("size-4 animate-spin", className)}
{...props} {...props}
/> />
) );
} }
export { Spinner } export { Spinner };

View File

@@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as SwitchPrimitive from "@radix-ui/react-switch";
import * as SwitchPrimitive from "@radix-ui/react-switch" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Switch({ function Switch({
className, className,
@@ -14,18 +14,18 @@ function Switch({
data-slot="switch" data-slot="switch"
className={cn( className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", "peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className className,
)} )}
{...props} {...props}
> >
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
data-slot="switch-thumb" data-slot="switch-thumb"
className={cn( className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0" "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0",
)} )}
/> />
</SwitchPrimitive.Root> </SwitchPrimitive.Root>
) );
} }
export { Switch } export { Switch };

View File

@@ -1,22 +1,19 @@
"use client" "use client";
import * as React from "react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Table({ className, ...props }: React.ComponentProps<"table">) { function Table({ className, ...props }: React.ComponentProps<"table">) {
return ( return (
<div <div data-slot="table-container" className="relative w-full overflow-x-auto">
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table <table
data-slot="table" data-slot="table"
className={cn("w-full caption-bottom text-sm", className)} className={cn("w-full caption-bottom text-sm", className)}
{...props} {...props}
/> />
</div> </div>
) );
} }
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
@@ -26,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
className={cn("[&_tr]:border-b", className)} className={cn("[&_tr]:border-b", className)}
{...props} {...props}
/> />
) );
} }
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
@@ -36,7 +33,7 @@ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
className={cn("[&_tr:last-child]:border-0", className)} className={cn("[&_tr:last-child]:border-0", className)}
{...props} {...props}
/> />
) );
} }
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
@@ -45,11 +42,11 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
data-slot="table-footer" data-slot="table-footer"
className={cn( className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function TableRow({ className, ...props }: React.ComponentProps<"tr">) { function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
@@ -58,11 +55,11 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
data-slot="table-row" data-slot="table-row"
className={cn( className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function TableHead({ className, ...props }: React.ComponentProps<"th">) { function TableHead({ className, ...props }: React.ComponentProps<"th">) {
@@ -71,11 +68,11 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
data-slot="table-head" data-slot="table-head"
className={cn( className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function TableCell({ className, ...props }: React.ComponentProps<"td">) { function TableCell({ className, ...props }: React.ComponentProps<"td">) {
@@ -84,11 +81,11 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
data-slot="table-cell" data-slot="table-cell"
className={cn( className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function TableCaption({ function TableCaption({
@@ -101,7 +98,7 @@ function TableCaption({
className={cn("text-muted-foreground mt-4 text-sm", className)} className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props} {...props}
/> />
) );
} }
export { export {
@@ -113,4 +110,4 @@ export {
TableRow, TableRow,
TableCell, TableCell,
TableCaption, TableCaption,
} };

View File

@@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as TabsPrimitive from "@radix-ui/react-tabs" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Tabs({ function Tabs({
className, className,
@@ -15,7 +15,7 @@ function Tabs({
className={cn("flex flex-col gap-2", className)} className={cn("flex flex-col gap-2", className)}
{...props} {...props}
/> />
) );
} }
function TabsList({ function TabsList({
@@ -27,11 +27,11 @@ function TabsList({
data-slot="tabs-list" data-slot="tabs-list"
className={cn( className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]", "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function TabsTrigger({ function TabsTrigger({
@@ -43,11 +43,11 @@ function TabsTrigger({
data-slot="tabs-trigger" data-slot="tabs-trigger"
className={cn( className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
function TabsContent({ function TabsContent({
@@ -60,7 +60,7 @@ function TabsContent({
className={cn("flex-1 outline-none", className)} className={cn("flex-1 outline-none", className)}
{...props} {...props}
/> />
) );
} }
export { Tabs, TabsList, TabsTrigger, TabsContent } export { Tabs, TabsList, TabsTrigger, TabsContent };

View File

@@ -1,6 +1,6 @@
import * as React from "react" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return ( return (
@@ -8,11 +8,11 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
data-slot="textarea" data-slot="textarea"
className={cn( 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", "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",
className className,
)} )}
{...props} {...props}
/> />
) );
} }
export { Textarea } export { Textarea };

View File

@@ -1,21 +1,20 @@
"use client" "use client";
import * as React from "react" import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" import type { VariantProps } from "class-variance-authority";
import { type VariantProps } from "class-variance-authority" import * as React from "react";
import { toggleVariants } from "@/components/ui/toggle";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext< const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants> & { VariantProps<typeof toggleVariants> & {
spacing?: number spacing?: number;
} }
>({ >({
size: "default", size: "default",
variant: "default", variant: "default",
spacing: 0, spacing: 0,
}) });
function ToggleGroup({ function ToggleGroup({
className, className,
@@ -26,7 +25,7 @@ function ToggleGroup({
...props ...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> & }: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants> & { VariantProps<typeof toggleVariants> & {
spacing?: number spacing?: number;
}) { }) {
return ( return (
<ToggleGroupPrimitive.Root <ToggleGroupPrimitive.Root
@@ -37,7 +36,7 @@ function ToggleGroup({
style={{ "--gap": spacing } as React.CSSProperties} style={{ "--gap": spacing } as React.CSSProperties}
className={cn( className={cn(
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs", "group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
className className,
)} )}
{...props} {...props}
> >
@@ -45,7 +44,7 @@ function ToggleGroup({
{children} {children}
</ToggleGroupContext.Provider> </ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root> </ToggleGroupPrimitive.Root>
) );
} }
function ToggleGroupItem({ function ToggleGroupItem({
@@ -56,7 +55,7 @@ function ToggleGroupItem({
...props ...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> & }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) { VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext) const context = React.useContext(ToggleGroupContext);
return ( return (
<ToggleGroupPrimitive.Item <ToggleGroupPrimitive.Item
@@ -71,13 +70,13 @@ function ToggleGroupItem({
}), }),
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10", "w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l", "data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
</ToggleGroupPrimitive.Item> </ToggleGroupPrimitive.Item>
) );
} }
export { ToggleGroup, ToggleGroupItem } export { ToggleGroup, ToggleGroupItem };

View File

@@ -1,10 +1,10 @@
"use client" "use client";
import * as React from "react" import * as TogglePrimitive from "@radix-ui/react-toggle";
import * as TogglePrimitive from "@radix-ui/react-toggle" import { cva, type VariantProps } from "class-variance-authority";
import { cva, type VariantProps } from "class-variance-authority" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const toggleVariants = cva( const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap", "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
@@ -25,8 +25,8 @@ const toggleVariants = cva(
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) );
function Toggle({ function Toggle({
className, className,
@@ -41,7 +41,7 @@ function Toggle({
className={cn(toggleVariants({ variant, size, className }))} className={cn(toggleVariants({ variant, size, className }))}
{...props} {...props}
/> />
) );
} }
export { Toggle, toggleVariants } export { Toggle, toggleVariants };

View File

@@ -1,9 +1,9 @@
"use client" "use client";
import * as React from "react" import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import type * as React from "react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function TooltipProvider({ function TooltipProvider({
delayDuration = 0, delayDuration = 0,
@@ -15,7 +15,7 @@ function TooltipProvider({
delayDuration={delayDuration} delayDuration={delayDuration}
{...props} {...props}
/> />
) );
} }
function Tooltip({ function Tooltip({
@@ -25,13 +25,13 @@ function Tooltip({
<TooltipProvider> <TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} /> <TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider> </TooltipProvider>
) );
} }
function TooltipTrigger({ function TooltipTrigger({
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) { }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} /> return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
} }
function TooltipContent({ function TooltipContent({
@@ -47,7 +47,7 @@ function TooltipContent({
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className className,
)} )}
{...props} {...props}
> >
@@ -55,7 +55,7 @@ function TooltipContent({
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content> </TooltipPrimitive.Content>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
) );
} }
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@@ -1,19 +1,19 @@
import * as React from "react" import * as React from "react";
const MOBILE_BREAKPOINT = 768 const MOBILE_BREAKPOINT = 768;
export function useIsMobile() { export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined) const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
React.useEffect(() => { React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => { const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
} };
mql.addEventListener("change", onChange) mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange) return () => mql.removeEventListener("change", onChange);
}, []) }, []);
return !!isMobile return !!isMobile;
} }

View File

@@ -32,7 +32,7 @@ api.interceptors.response.use(
errorCache.set(url, now); errorCache.set(url, now);
return Promise.reject(error); return Promise.reject(error);
} },
); );
export default api; export default api;

View File

@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx" import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs));
} }

View File

@@ -1,18 +1,19 @@
"use client"; "use client";
import * as React from "react";
import { UserService } from "@/services/user.service";
import { AuthService } from "@/services/auth.service";
import type { User } from "@/types/user";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import * as React from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { AuthService } from "@/services/auth.service";
import { UserService } from "@/services/user.service";
import type { RegisterPayload } from "@/types/auth";
import type { User } from "@/types/user";
interface AuthContextType { interface AuthContextType {
user: User | null; user: User | null;
isLoading: boolean; isLoading: boolean;
isAuthenticated: boolean; isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>; login: (email: string, password: string) => Promise<void>;
register: (payload: any) => Promise<void>; register: (payload: RegisterPayload) => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
refreshUser: () => Promise<void>; refreshUser: () => Promise<void>;
} }
@@ -28,7 +29,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
try { try {
const userData = await UserService.getMe(); const userData = await UserService.getMe();
setUser(userData); setUser(userData);
} catch (error) { } catch (_error) {
setUser(null); setUser(null);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -45,19 +46,51 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
await refreshUser(); await refreshUser();
toast.success("Connexion réussie !"); toast.success("Connexion réussie !");
router.push("/"); router.push("/");
} catch (error: any) { } catch (error: unknown) {
toast.error(error.response?.data?.message || "Erreur de connexion"); let errorMessage = "Erreur de connexion";
if (
error &&
typeof error === "object" &&
"response" in error &&
error.response &&
typeof error.response === "object" &&
"data" in error.response &&
error.response.data &&
typeof error.response.data === "object" &&
"message" in error.response.data &&
typeof error.response.data.message === "string"
) {
errorMessage = error.response.data.message;
}
toast.error(errorMessage);
throw error; throw error;
} }
}; };
const register = async (payload: any) => { const register = async (payload: RegisterPayload) => {
try { try {
await AuthService.register(payload); await AuthService.register(payload);
toast.success("Inscription réussie ! Vous pouvez maintenant vous connecter."); toast.success(
"Inscription réussie ! Vous pouvez maintenant vous connecter.",
);
router.push("/login"); router.push("/login");
} catch (error: any) { } catch (error: unknown) {
toast.error(error.response?.data?.message || "Erreur d'inscription"); let errorMessage = "Erreur d'inscription";
if (
error &&
typeof error === "object" &&
"response" in error &&
error.response &&
typeof error.response === "object" &&
"data" in error.response &&
error.response.data &&
typeof error.response.data === "object" &&
"message" in error.response.data &&
typeof error.response.data.message === "string"
) {
errorMessage = error.response.data.message;
}
toast.error(errorMessage);
throw error; throw error;
} }
}; };
@@ -68,7 +101,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setUser(null); setUser(null);
toast.success("Déconnexion réussie"); toast.success("Déconnexion réussie");
router.push("/"); router.push("/");
} catch (error) { } catch (_error) {
toast.error("Erreur lors de la déconnexion"); toast.error("Erreur lors de la déconnexion");
} }
}; };

View File

@@ -1,15 +1,17 @@
import api from "@/lib/api"; import api from "@/lib/api";
import type { LoginResponse } from "@/types/auth"; import type { LoginResponse, RegisterPayload } from "@/types/auth";
export const AuthService = { export const AuthService = {
async login(email: string, password: string): Promise<LoginResponse> { async login(email: string, password: string): Promise<LoginResponse> {
const { data } = await api.post<LoginResponse>("/auth/login", { email, password }); const { data } = await api.post<LoginResponse>("/auth/login", {
email,
password,
});
return data; return data;
}, },
async register(payload: any): Promise<any> { async register(payload: RegisterPayload): Promise<void> {
const { data } = await api.post("/auth/register", payload); await api.post("/auth/register", payload);
return data;
}, },
async logout(): Promise<void> { async logout(): Promise<void> {

View File

@@ -11,23 +11,32 @@ export const ContentService = {
query?: string; query?: string;
author?: string; author?: string;
}): Promise<PaginatedResponse<Content>> { }): Promise<PaginatedResponse<Content>> {
const { data } = await api.get<PaginatedResponse<Content>>("/contents/explore", { const { data } = await api.get<PaginatedResponse<Content>>(
"/contents/explore",
{
params, params,
}); },
);
return data; return data;
}, },
async getTrends(limit = 10, offset = 0): Promise<PaginatedResponse<Content>> { async getTrends(limit = 10, offset = 0): Promise<PaginatedResponse<Content>> {
const { data } = await api.get<PaginatedResponse<Content>>("/contents/trends", { const { data } = await api.get<PaginatedResponse<Content>>(
"/contents/trends",
{
params: { limit, offset }, params: { limit, offset },
}); },
);
return data; return data;
}, },
async getRecent(limit = 10, offset = 0): Promise<PaginatedResponse<Content>> { async getRecent(limit = 10, offset = 0): Promise<PaginatedResponse<Content>> {
const { data } = await api.get<PaginatedResponse<Content>>("/contents/recent", { const { data } = await api.get<PaginatedResponse<Content>>(
"/contents/recent",
{
params: { limit, offset }, params: { limit, offset },
}); },
);
return data; return data;
}, },

View File

@@ -10,8 +10,13 @@ export const FavoriteService = {
await api.delete(`/favorites/${contentId}`); await api.delete(`/favorites/${contentId}`);
}, },
async list(params: { limit: number; offset: number }): Promise<PaginatedResponse<Content>> { async list(params: {
const { data } = await api.get<PaginatedResponse<Content>>("/favorites", { params }); limit: number;
offset: number;
}): Promise<PaginatedResponse<Content>> {
const { data } = await api.get<PaginatedResponse<Content>>("/favorites", {
params,
});
return data; return data;
}, },
}; };

View File

@@ -1,5 +1,5 @@
import api from "@/lib/api"; import api from "@/lib/api";
import type { User, UserProfile } from "@/types/user"; import type { User } from "@/types/user";
export const UserService = { export const UserService = {
async getMe(): Promise<User> { async getMe(): Promise<User> {

View File

@@ -3,6 +3,13 @@ export interface LoginResponse {
userId: string; userId: string;
} }
export interface RegisterPayload {
username: string;
email: string;
password: string;
displayName?: string;
}
export interface AuthStatus { export interface AuthStatus {
isAuthenticated: boolean; isAuthenticated: boolean;
user: null | { user: null | {

View File

@@ -37,7 +37,6 @@ export interface Category {
description?: string; description?: string;
} }
export interface PaginatedResponse<T> { export interface PaginatedResponse<T> {
data: T[]; data: T[];
total: number; total: number;