init: kovakemlak-crm project scaffold
- Next.js 16 + Appwrite multi-tenant emlak CRM - Database: kovakemlak-db (properties, customers, customer_searches, property_matches, presentations, investors, activities, tenant_settings) - Same stack as isletmem-kovakcrm (shadcn/ui template base) - Modules: portföy, müşteri takibi, arama kriterleri, otomatik eşleştirme, sunum linki, yatırımcı portalı
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
Briefcase,
|
||||
Calendar,
|
||||
CheckSquare,
|
||||
CreditCard,
|
||||
FileText,
|
||||
LayoutDashboard,
|
||||
Package,
|
||||
Receipt,
|
||||
Settings,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Wallet,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Logo } from "@/components/logo";
|
||||
import { NavMain } from "@/components/nav-main";
|
||||
import { NavUser } from "@/components/nav-user";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
import type { ShellCompany, ShellUser } from "@/app/(dashboard)/dashboard-shell";
|
||||
|
||||
const navGroups = [
|
||||
{
|
||||
label: "Genel",
|
||||
items: [
|
||||
{
|
||||
title: "Genel bakış",
|
||||
url: "/dashboard",
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "İşletme",
|
||||
items: [
|
||||
{
|
||||
title: "Müşteri Adayları",
|
||||
url: "/leads",
|
||||
icon: TrendingUp,
|
||||
},
|
||||
{
|
||||
title: "Müşteriler",
|
||||
url: "/customers",
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: "Hizmetler",
|
||||
url: "/services",
|
||||
icon: Briefcase,
|
||||
},
|
||||
{
|
||||
title: "Yazılımlarımız",
|
||||
url: "/software",
|
||||
icon: Package,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Operasyon",
|
||||
items: [
|
||||
{
|
||||
title: "Takvim",
|
||||
url: "/calendar",
|
||||
icon: Calendar,
|
||||
},
|
||||
{
|
||||
title: "Görevler",
|
||||
url: "/tasks",
|
||||
icon: CheckSquare,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Finans",
|
||||
items: [
|
||||
{
|
||||
title: "Gelir / Gider",
|
||||
url: "/finance",
|
||||
icon: Wallet,
|
||||
},
|
||||
{
|
||||
title: "Bankalar",
|
||||
url: "/finance/banks",
|
||||
icon: Briefcase,
|
||||
items: [
|
||||
{ title: "Banka hesapları", url: "/finance/banks" },
|
||||
{ title: "Krediler", url: "/finance/loans" },
|
||||
{ title: "Kredi kartları", url: "/finance/cards" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Faturalar",
|
||||
url: "/invoices",
|
||||
icon: Receipt,
|
||||
},
|
||||
{
|
||||
title: "Rapor",
|
||||
url: "/finance/reports",
|
||||
icon: FileText,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Hesap",
|
||||
items: [
|
||||
{
|
||||
title: "Çalışma alanı",
|
||||
url: "/settings/workspace",
|
||||
icon: Settings,
|
||||
items: [
|
||||
{ title: "Şirket bilgileri", url: "/settings/workspace" },
|
||||
{ title: "Ekip üyeleri", url: "/settings/members" },
|
||||
{ title: "Faturalama", url: "/settings/billing" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Profil",
|
||||
url: "/settings/account",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
title: "Plan",
|
||||
url: "/pricing",
|
||||
icon: CreditCard,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function AppSidebar({
|
||||
user,
|
||||
company,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Sidebar> & {
|
||||
user: ShellUser;
|
||||
company: ShellCompany;
|
||||
}) {
|
||||
return (
|
||||
<Sidebar {...props}>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" asChild>
|
||||
<Link href="/dashboard">
|
||||
{company.logoUrl ? (
|
||||
<div className="bg-background flex aspect-square size-8 items-center justify-center overflow-hidden rounded-lg border">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={company.logoUrl}
|
||||
alt={`${company.name} logo`}
|
||||
className="size-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-primary text-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
|
||||
<Logo size={20} className="text-current" />
|
||||
</div>
|
||||
)}
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">İşletmem</span>
|
||||
<span className="text-muted-foreground truncate text-xs">{company.name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
{navGroups.map((group) => (
|
||||
<NavMain key={group.label} label={group.label} items={group.items} />
|
||||
))}
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={user} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Crown } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onOpenChange: (v: boolean) => void;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export function PlanLimitDialog({ open, onOpenChange, message }: Props) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Crown className="size-5 text-amber-500" />
|
||||
Ücretsiz plan sınırına ulaştınız
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{message ?? "Yeni kayıt eklemek için Pro plana geçmeniz gerekiyor."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="rounded-md border bg-muted/30 p-3 text-sm space-y-1">
|
||||
<div className="font-medium text-foreground">Pro plan ile gelen avantajlar</div>
|
||||
<ul className="text-muted-foreground space-y-0.5 list-disc list-inside">
|
||||
<li>Sınırsız müşteri, finans kaydı, yazılım</li>
|
||||
<li>Sınırsız ekip üyesi</li>
|
||||
<li>Audit log + öncelikli destek</li>
|
||||
</ul>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Kapat
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link href="/settings/billing">
|
||||
<Crown className="size-4" />
|
||||
Pro'ya geç
|
||||
</Link>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import Link from "next/link";
|
||||
import { AlertTriangle, Crown } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
RESOURCE_LABELS,
|
||||
type PlanResource,
|
||||
type PlanUsage,
|
||||
} from "@/lib/appwrite/plan-limits";
|
||||
|
||||
const SOFT_THRESHOLD = 0.8;
|
||||
|
||||
export function UsageBanner({
|
||||
usage,
|
||||
resource,
|
||||
}: {
|
||||
usage: PlanUsage;
|
||||
resource: PlanResource;
|
||||
}) {
|
||||
if (usage.plan === "pro") return null;
|
||||
|
||||
const u = usage.usage[resource];
|
||||
if (u.limit === Number.POSITIVE_INFINITY) return null;
|
||||
|
||||
const ratio = u.used / u.limit;
|
||||
if (ratio < SOFT_THRESHOLD) return null;
|
||||
|
||||
const label = RESOURCE_LABELS[resource];
|
||||
const reached = u.reached;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={
|
||||
reached
|
||||
? "border-destructive/40 bg-destructive/5"
|
||||
: "border-amber-500/40 bg-amber-500/5"
|
||||
}
|
||||
>
|
||||
<CardContent className="flex flex-col gap-3 py-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<AlertTriangle
|
||||
className={`size-4 mt-0.5 shrink-0 ${
|
||||
reached ? "text-destructive" : "text-amber-600"
|
||||
}`}
|
||||
/>
|
||||
<div>
|
||||
<span className="font-medium">
|
||||
{reached ? "Sınıra ulaşıldı" : "Sınıra yaklaşıyorsun"}:
|
||||
</span>{" "}
|
||||
<span className="text-muted-foreground">
|
||||
{u.used} / {u.limit} {label}.
|
||||
</span>
|
||||
{reached && (
|
||||
<span className="text-muted-foreground"> Yeni {label} eklemek için Pro'ya geç.</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button asChild size="sm" variant={reached ? "default" : "outline"}>
|
||||
<Link href="/settings/billing">
|
||||
<Crown className="size-3.5" />
|
||||
Pro'ya geç
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
interface ColorPickerProps {
|
||||
label: string
|
||||
cssVar: string
|
||||
value: string
|
||||
onChange: (cssVar: string, value: string) => void
|
||||
}
|
||||
|
||||
export function ColorPicker({ label, cssVar, value, onChange }: ColorPickerProps) {
|
||||
const [localValue, setLocalValue] = React.useState(value)
|
||||
|
||||
React.useEffect(() => {
|
||||
setLocalValue(value)
|
||||
}, [value])
|
||||
|
||||
const handleColorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newColor = e.target.value
|
||||
setLocalValue(newColor)
|
||||
onChange(cssVar, newColor)
|
||||
}
|
||||
|
||||
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value
|
||||
setLocalValue(newValue)
|
||||
onChange(cssVar, newValue)
|
||||
}
|
||||
|
||||
// Get current computed color for display
|
||||
const displayColor = React.useMemo(() => {
|
||||
if (localValue && localValue.startsWith('#')) {
|
||||
return localValue
|
||||
}
|
||||
|
||||
// Try to get computed value from CSS
|
||||
const computed = getComputedStyle(document.documentElement).getPropertyValue(cssVar).trim()
|
||||
if (computed && computed.startsWith('#')) {
|
||||
return computed
|
||||
}
|
||||
|
||||
return '#000000'
|
||||
}, [localValue, cssVar])
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`color-${cssVar}`} className="text-xs font-medium">
|
||||
{label}
|
||||
</Label>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="relative">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0 overflow-hidden cursor-pointer"
|
||||
style={{ backgroundColor: displayColor }}
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
id={`color-${cssVar}`}
|
||||
value={displayColor}
|
||||
onChange={handleColorChange}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={`${cssVar} value`}
|
||||
value={localValue}
|
||||
onChange={handleTextChange}
|
||||
className="h-8 text-xs flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import {
|
||||
Briefcase,
|
||||
Calendar as CalendarIcon,
|
||||
CheckSquare,
|
||||
CircleDollarSign,
|
||||
FilePlus,
|
||||
LayoutDashboard,
|
||||
Loader2,
|
||||
Package,
|
||||
Receipt,
|
||||
Search,
|
||||
Settings,
|
||||
UserPlus,
|
||||
Users,
|
||||
Wallet,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
globalSearchAction,
|
||||
type SearchGroup,
|
||||
type SearchHit,
|
||||
type SearchResults,
|
||||
} from "@/lib/appwrite/search-actions";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-xl",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
shouldFilter={false}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-4">
|
||||
<Search className="text-muted-foreground mr-2 size-4" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-12 w-full border-none bg-transparent py-3 text-base outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[440px] overflow-y-auto overflow-x-hidden p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="text-muted-foreground py-8 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&:not(:first-child)]:mt-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground relative flex cursor-pointer select-none items-center gap-2 rounded-md px-3 py-2 text-sm outline-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
type NavItem = { key: string; title: string; url: string; icon: LucideIcon };
|
||||
|
||||
const NAV: NavItem[] = [
|
||||
{ key: "nav-dashboard", title: "Genel bakış", url: "/dashboard", icon: LayoutDashboard },
|
||||
{ key: "nav-customers", title: "Müşteriler", url: "/customers", icon: Users },
|
||||
{ key: "nav-services", title: "Hizmetler", url: "/services", icon: Briefcase },
|
||||
{ key: "nav-software", title: "Yazılımlarımız", url: "/software", icon: Package },
|
||||
{ key: "nav-calendar", title: "Takvim", url: "/calendar", icon: CalendarIcon },
|
||||
{ key: "nav-tasks", title: "Görevler", url: "/tasks", icon: CheckSquare },
|
||||
{ key: "nav-finance", title: "Gelir / Gider", url: "/finance", icon: Wallet },
|
||||
{ key: "nav-invoices", title: "Faturalar", url: "/invoices", icon: Receipt },
|
||||
];
|
||||
|
||||
const QUICK_ACTIONS: NavItem[] = [
|
||||
{ key: "qa-customer", title: "Yeni müşteri ekle", url: "/customers", icon: UserPlus },
|
||||
{ key: "qa-invoice", title: "Yeni fatura kes", url: "/invoices", icon: Receipt },
|
||||
{ key: "qa-task", title: "Yeni görev oluştur", url: "/tasks", icon: FilePlus },
|
||||
{ key: "qa-event", title: "Takvime etkinlik ekle", url: "/calendar", icon: CalendarIcon },
|
||||
{ key: "qa-finance", title: "Yeni gelir/gider girişi", url: "/finance", icon: CircleDollarSign },
|
||||
];
|
||||
|
||||
const SETTINGS_NAV: NavItem[] = [
|
||||
{ key: "set-workspace", title: "Şirket bilgileri", url: "/settings/workspace", icon: Settings },
|
||||
{ key: "set-members", title: "Ekip üyeleri", url: "/settings/members", icon: Users },
|
||||
{ key: "set-account", title: "Profil", url: "/settings/account", icon: Settings },
|
||||
];
|
||||
|
||||
const GROUP_LABEL: Record<SearchGroup, string> = {
|
||||
customers: "Müşteriler",
|
||||
invoices: "Faturalar",
|
||||
tasks: "Görevler",
|
||||
services: "Hizmetler",
|
||||
software: "Yazılımlar",
|
||||
events: "Takvim",
|
||||
finance: "Finans",
|
||||
};
|
||||
|
||||
const GROUP_ICON: Record<SearchGroup, LucideIcon> = {
|
||||
customers: Users,
|
||||
invoices: Receipt,
|
||||
tasks: CheckSquare,
|
||||
services: Briefcase,
|
||||
software: Package,
|
||||
events: CalendarIcon,
|
||||
finance: Wallet,
|
||||
};
|
||||
|
||||
const GROUP_ORDER: SearchGroup[] = [
|
||||
"customers",
|
||||
"invoices",
|
||||
"tasks",
|
||||
"events",
|
||||
"finance",
|
||||
"services",
|
||||
"software",
|
||||
];
|
||||
|
||||
interface CommandSearchProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
function filterNav(items: NavItem[], q: string): NavItem[] {
|
||||
if (!q) return items;
|
||||
const lower = q.toLocaleLowerCase("tr-TR");
|
||||
return items.filter((i) => i.title.toLocaleLowerCase("tr-TR").includes(lower));
|
||||
}
|
||||
|
||||
export function CommandSearch({ open, onOpenChange }: CommandSearchProps) {
|
||||
const router = useRouter();
|
||||
const [query, setQuery] = React.useState("");
|
||||
const [results, setResults] = React.useState<SearchResults | null>(null);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
setQuery("");
|
||||
setResults(null);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const trimmed = query.trim();
|
||||
if (trimmed.length < 2) {
|
||||
setResults(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const t = setTimeout(async () => {
|
||||
try {
|
||||
const r = await globalSearchAction(trimmed);
|
||||
setResults(r);
|
||||
} catch {
|
||||
setResults(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, 220);
|
||||
return () => clearTimeout(t);
|
||||
}, [query]);
|
||||
|
||||
const handleSelect = (url: string) => {
|
||||
router.push(url);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const navMatches = filterNav(NAV, query);
|
||||
const quickMatches = filterNav(QUICK_ACTIONS, query);
|
||||
const settingsMatches = filterNav(SETTINGS_NAV, query);
|
||||
|
||||
const totalEntityHits = results
|
||||
? GROUP_ORDER.reduce((s, g) => s + results[g].length, 0)
|
||||
: 0;
|
||||
|
||||
const showEmpty =
|
||||
query.trim().length >= 2 &&
|
||||
!loading &&
|
||||
totalEntityHits === 0 &&
|
||||
navMatches.length === 0 &&
|
||||
quickMatches.length === 0 &&
|
||||
settingsMatches.length === 0;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[640px] overflow-hidden border p-0 shadow-2xl">
|
||||
<DialogTitle className="sr-only">Arama</DialogTitle>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Arayın: müşteri, fatura, görev, etkinlik, hizmet..."
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
autoFocus
|
||||
/>
|
||||
<CommandList>
|
||||
{loading && (
|
||||
<div className="text-muted-foreground flex items-center justify-center py-6 text-sm">
|
||||
<Loader2 className="mr-2 size-3.5 animate-spin" />
|
||||
Aranıyor...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showEmpty && <CommandEmpty>Sonuç bulunamadı.</CommandEmpty>}
|
||||
|
||||
{results &&
|
||||
!loading &&
|
||||
GROUP_ORDER.map((group) => {
|
||||
const hits = results[group];
|
||||
if (hits.length === 0) return null;
|
||||
const Icon = GROUP_ICON[group];
|
||||
return (
|
||||
<CommandGroup key={group} heading={GROUP_LABEL[group]}>
|
||||
{hits.map((h: SearchHit) => (
|
||||
<CommandItem
|
||||
key={`${group}-${h.id}`}
|
||||
value={`${group}-${h.id}-${h.title}`}
|
||||
onSelect={() => handleSelect(h.url)}
|
||||
>
|
||||
<Icon className="text-muted-foreground size-4" />
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="truncate">{h.title}</span>
|
||||
{h.subtitle && (
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{h.subtitle}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
);
|
||||
})}
|
||||
|
||||
{navMatches.length > 0 && (
|
||||
<CommandGroup heading="Sayfalar">
|
||||
{navMatches.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<CommandItem
|
||||
key={item.key}
|
||||
value={item.key}
|
||||
onSelect={() => handleSelect(item.url)}
|
||||
>
|
||||
<Icon className="text-muted-foreground size-4" />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{quickMatches.length > 0 && (
|
||||
<CommandGroup heading="Hızlı aksiyonlar">
|
||||
{quickMatches.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<CommandItem
|
||||
key={item.key}
|
||||
value={item.key}
|
||||
onSelect={() => handleSelect(item.url)}
|
||||
>
|
||||
<Icon className="text-muted-foreground size-4" />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{settingsMatches.length > 0 && (
|
||||
<CommandGroup heading="Ayarlar">
|
||||
{settingsMatches.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<CommandItem
|
||||
key={item.key}
|
||||
value={item.key}
|
||||
onSelect={() => handleSelect(item.url)}
|
||||
>
|
||||
<Icon className="text-muted-foreground size-4" />
|
||||
{item.title}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
|
||||
<div className="text-muted-foreground bg-muted/30 flex items-center justify-between gap-2 border-t px-4 py-2 text-[11px]">
|
||||
<span>↵ Aç · ↑↓ Gez · Esc Kapat</span>
|
||||
<kbd className="bg-background rounded border px-1.5 py-0.5 font-mono">⌘K</kbd>
|
||||
</div>
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchTrigger({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="border-input bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring relative inline-flex h-8 w-full items-center justify-start gap-2 whitespace-nowrap rounded-md border px-3 py-1 text-sm font-medium shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 sm:pr-12 md:w-44 lg:w-64"
|
||||
>
|
||||
<Search className="mr-1 h-3.5 w-3.5" />
|
||||
<span>Hızlı ara...</span>
|
||||
<kbd className="bg-muted pointer-events-none absolute right-1.5 top-1.5 hidden h-4 select-none items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
|
||||
<span className="text-xs">⌘</span>K
|
||||
</kbd>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface DotPatternProps {
|
||||
className?: string
|
||||
size?: "sm" | "md" | "lg"
|
||||
opacity?: "low" | "medium" | "high"
|
||||
fadeStyle?: "ellipse" | "circle" | "none"
|
||||
}
|
||||
|
||||
export function DotPattern({
|
||||
className,
|
||||
size = "md",
|
||||
opacity = "medium",
|
||||
fadeStyle = "ellipse"
|
||||
}: DotPatternProps) {
|
||||
// Use predefined Tailwind classes instead of template literals for Next.js compatibility
|
||||
const sizeMap = {
|
||||
sm: "[background-size:12px_12px]",
|
||||
md: "[background-size:16px_16px]",
|
||||
lg: "[background-size:20px_20px]"
|
||||
}
|
||||
|
||||
const opacityMap = {
|
||||
low: "opacity-30",
|
||||
medium: "opacity-50",
|
||||
high: "opacity-70"
|
||||
}
|
||||
|
||||
const fadeMap = {
|
||||
ellipse: "[mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]",
|
||||
circle: "[mask-image:radial-gradient(circle_at_50%_50%,#000_70%,transparent_100%)]",
|
||||
none: ""
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] dark:bg-[radial-gradient(#374151_1px,transparent_1px)]",
|
||||
sizeMap[size],
|
||||
fadeMap[fadeStyle],
|
||||
opacityMap[opacity],
|
||||
className
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Alternative themed variants
|
||||
export function DotPatternLight({
|
||||
className,
|
||||
size = "md",
|
||||
opacity = "medium",
|
||||
fadeStyle = "ellipse"
|
||||
}: DotPatternProps) {
|
||||
// Use predefined Tailwind classes instead of template literals for Next.js compatibility
|
||||
const sizeMap = {
|
||||
sm: "[background-size:12px_12px]",
|
||||
md: "[background-size:16px_16px]",
|
||||
lg: "[background-size:20px_20px]"
|
||||
}
|
||||
|
||||
const opacityMap = {
|
||||
low: "opacity-20",
|
||||
medium: "opacity-40",
|
||||
high: "opacity-60"
|
||||
}
|
||||
|
||||
const fadeMap = {
|
||||
ellipse: "[mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]",
|
||||
circle: "[mask-image:radial-gradient(circle_at_50%_50%,#000_70%,transparent_100%)]",
|
||||
none: ""
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 bg-[radial-gradient(#d1d5db_1px,transparent_1px)]",
|
||||
sizeMap[size],
|
||||
fadeMap[fadeStyle],
|
||||
opacityMap[opacity],
|
||||
className
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function DotPatternDark({
|
||||
className,
|
||||
size = "md",
|
||||
opacity = "medium",
|
||||
fadeStyle = "ellipse"
|
||||
}: DotPatternProps) {
|
||||
// Use predefined Tailwind classes instead of template literals for Next.js compatibility
|
||||
const sizeMap = {
|
||||
sm: "[background-size:12px_12px]",
|
||||
md: "[background-size:16px_16px]",
|
||||
lg: "[background-size:20px_20px]"
|
||||
}
|
||||
|
||||
const opacityMap = {
|
||||
low: "opacity-30",
|
||||
medium: "opacity-50",
|
||||
high: "opacity-70"
|
||||
}
|
||||
|
||||
const fadeMap = {
|
||||
ellipse: "[mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]",
|
||||
circle: "[mask-image:radial-gradient(circle_at_50%_50%,#000_70%,transparent_100%)]",
|
||||
none: ""
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 bg-[radial-gradient(#4b5563_1px,transparent_1px)]",
|
||||
sizeMap[size],
|
||||
fadeMap[fadeStyle],
|
||||
opacityMap[opacity],
|
||||
className
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import dynamic from 'next/dynamic'
|
||||
import React from 'react'
|
||||
|
||||
// Heavy components that should be dynamically imported
|
||||
export const DynamicThemeCustomizer = dynamic(() => import('./theme-customizer').then(mod => ({ default: mod.ThemeCustomizer })), {
|
||||
ssr: false,
|
||||
loading: () => React.createElement('div', { className: "h-8 w-8 animate-pulse bg-muted rounded" })
|
||||
})
|
||||
|
||||
export const DynamicColorPicker = dynamic(() => import('./color-picker').then(mod => ({ default: mod.ColorPicker })), {
|
||||
ssr: false,
|
||||
loading: () => React.createElement('div', { className: "h-8 w-8 animate-pulse bg-muted rounded" })
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Building2, User } from "lucide-react";
|
||||
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Scope = "company" | "personal";
|
||||
|
||||
export function ScopeToggle({
|
||||
name = "scope",
|
||||
defaultValue = "company",
|
||||
label = "Kapsam",
|
||||
description,
|
||||
}: {
|
||||
name?: string;
|
||||
defaultValue?: Scope;
|
||||
label?: string;
|
||||
description?: string;
|
||||
}) {
|
||||
const [value, setValue] = useState<Scope>(defaultValue);
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<Label>{label}</Label>
|
||||
<input type="hidden" name={name} value={value} />
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setValue("company")}
|
||||
className={cn(
|
||||
"border-input flex flex-col items-start gap-1 rounded-md border p-3 text-left transition-colors",
|
||||
value === "company"
|
||||
? "border-primary bg-primary/5"
|
||||
: "hover:bg-muted/50",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<Building2 className="size-4" />
|
||||
Şirket
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Ekipteki herkes görür ve düzenleyebilir.
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setValue("personal")}
|
||||
className={cn(
|
||||
"border-input flex flex-col items-start gap-1 rounded-md border p-3 text-left transition-colors",
|
||||
value === "personal"
|
||||
? "border-primary bg-primary/5"
|
||||
: "hover:bg-muted/50",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<User className="size-4" />
|
||||
Bireysel
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Yalnızca siz görürsünüz, ekibe yansımaz.
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
{description && <p className="text-muted-foreground text-xs">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ScopeBadge({ scope }: { scope?: Scope }) {
|
||||
if (scope === "personal") {
|
||||
return (
|
||||
<span className="bg-violet-500/15 text-violet-700 dark:text-violet-300 border-violet-500/30 inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[10px] font-medium">
|
||||
<User className="size-2.5" />
|
||||
Bireysel
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null; // Company is default — no badge needed
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import Image from 'next/image'
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface Image3DProps {
|
||||
lightSrc: string
|
||||
darkSrc: string
|
||||
alt: string
|
||||
className?: string
|
||||
direction?: "left" | "right"
|
||||
}
|
||||
|
||||
export function Image3D({
|
||||
lightSrc,
|
||||
darkSrc,
|
||||
alt,
|
||||
className,
|
||||
direction = "left"
|
||||
}: Image3DProps) {
|
||||
const isRight = direction === "right"
|
||||
|
||||
return (
|
||||
<div className={cn("group relative aspect-[4/3] w-full", className)}>
|
||||
<div className="perspective-distant transform-3d">
|
||||
{/* Animated background glow */}
|
||||
<div className="absolute sm:-inset-8 rounded-3xl bg-gradient-to-r from-primary/10 via-blue-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-all duration-1000 blur-2xl" />
|
||||
|
||||
{/* Main 3D container */}
|
||||
<div className="relative size-full transform-3d group-hover:rotate-x-8 group-hover:rotate-y-12 group-hover:translate-z-16 transition-all duration-700 ease-out">
|
||||
|
||||
{/* Depth layers for 3D effect */}
|
||||
<div className="absolute inset-0 translate-y-4 translate-x-2 -translate-z-8 rounded-2xl">
|
||||
<div className="size-full rounded-2xl bg-gradient-to-br from-primary/10 via-background/40 to-secondary/10 shadow-xl" />
|
||||
</div>
|
||||
|
||||
{/* Main image container */}
|
||||
<div className="relative z-10 size-full rounded-2xl overflow-hidden shadow-2xl shadow-primary/20">
|
||||
{/* Shimmer effect */}
|
||||
<div className={cn(
|
||||
"absolute inset-0 z-20 bg-gradient-to-r from-transparent via-white/20 to-transparent -skew-x-12 transition-transform duration-1000 ease-out pointer-events-none",
|
||||
isRight
|
||||
? "translate-x-full group-hover:-translate-x-full"
|
||||
: "-translate-x-full group-hover:translate-x-full"
|
||||
)} />
|
||||
|
||||
{/* Content fade mask */}
|
||||
<div className={cn(
|
||||
"absolute inset-0 z-15 pointer-events-none",
|
||||
isRight
|
||||
? "bg-linear-to-l from-background from-0% via-background/85 via-15% to-transparent to-40%"
|
||||
: "bg-linear-to-r from-background from-0% via-background/85 via-15% to-transparent to-40%"
|
||||
)} />
|
||||
|
||||
{/* Theme-aware images */}
|
||||
<Image
|
||||
src={lightSrc}
|
||||
alt={`${alt} - Light Mode`}
|
||||
width={800}
|
||||
height={600}
|
||||
className={cn(
|
||||
"block size-full object-cover dark:hidden transition-transform duration-700 group-hover:scale-105",
|
||||
isRight ? "object-center" : "object-left"
|
||||
)}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<Image
|
||||
src={darkSrc}
|
||||
alt={`${alt} - Dark Mode`}
|
||||
width={800}
|
||||
height={600}
|
||||
className={cn(
|
||||
"hidden dark:block size-full object-cover transition-transform duration-700 group-hover:scale-105",
|
||||
isRight ? "object-center" : "object-left"
|
||||
)}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* Border highlight */}
|
||||
<div className="absolute inset-0 rounded-2xl ring-1 ring-white/20 dark:ring-white/10 group-hover:ring-primary/40 transition-all duration-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Shield,
|
||||
BarChart3,
|
||||
Database,
|
||||
Building2,
|
||||
Rocket,
|
||||
Settings,
|
||||
Zap,
|
||||
Package,
|
||||
Layout,
|
||||
Crown,
|
||||
Palette
|
||||
} from 'lucide-react'
|
||||
|
||||
const menuSections = [
|
||||
{
|
||||
title: 'Browse Products',
|
||||
items: [
|
||||
{
|
||||
title: 'Free Blocks',
|
||||
description: 'Essential UI components and sections',
|
||||
icon: Package,
|
||||
href: '#free-blocks'
|
||||
},
|
||||
{
|
||||
title: 'Premium Templates',
|
||||
description: 'Complete page templates and layouts',
|
||||
icon: Crown,
|
||||
href: '#premium-templates'
|
||||
},
|
||||
{
|
||||
title: 'Admin Dashboards',
|
||||
description: 'Full-featured dashboard solutions',
|
||||
icon: BarChart3,
|
||||
href: '#admin-dashboards'
|
||||
},
|
||||
{
|
||||
title: 'Landing Pages',
|
||||
description: 'Marketing and product landing templates',
|
||||
icon: Layout,
|
||||
href: '#landing-pages'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Categories',
|
||||
items: [
|
||||
{
|
||||
title: 'E-commerce',
|
||||
description: 'Online store admin panels and components',
|
||||
icon: Building2,
|
||||
href: '#ecommerce'
|
||||
},
|
||||
{
|
||||
title: 'SaaS Dashboards',
|
||||
description: 'Application admin interfaces',
|
||||
icon: Rocket,
|
||||
href: '#saas-dashboards'
|
||||
},
|
||||
{
|
||||
title: 'Analytics',
|
||||
description: 'Data visualization and reporting templates',
|
||||
icon: BarChart3,
|
||||
href: '#analytics'
|
||||
},
|
||||
{
|
||||
title: 'Authentication',
|
||||
description: 'Login, signup, and user management pages',
|
||||
icon: Shield,
|
||||
href: '#authentication'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Resources',
|
||||
items: [
|
||||
{
|
||||
title: 'Documentation',
|
||||
description: 'Integration guides and setup instructions',
|
||||
icon: Database,
|
||||
href: '#docs'
|
||||
},
|
||||
{
|
||||
title: 'Component Showcase',
|
||||
description: 'Interactive preview of all components',
|
||||
icon: Palette,
|
||||
href: '#showcase'
|
||||
},
|
||||
{
|
||||
title: 'GitHub Repository',
|
||||
description: 'Open source foundation and community',
|
||||
icon: Settings,
|
||||
href: '#github'
|
||||
},
|
||||
{
|
||||
title: 'Design System',
|
||||
description: 'shadcn/ui standards and customization',
|
||||
icon: Zap,
|
||||
href: '#design-system'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export function MegaMenu() {
|
||||
return (
|
||||
<div className="w-[700px] max-w-[95vw] p-4 sm:p-6 lg:p-8 bg-background">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8 lg:gap-12">
|
||||
{menuSections.map((section) => (
|
||||
<div key={section.title} className="space-y-4 lg:space-y-6">
|
||||
{/* Section Header */}
|
||||
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
|
||||
{section.title}
|
||||
</h3>
|
||||
|
||||
{/* Section Links */}
|
||||
<div className="space-y-3 lg:space-y-4">
|
||||
{section.items.map((item) => (
|
||||
<a
|
||||
key={item.title}
|
||||
href={item.href}
|
||||
className="group block space-y-1 lg:space-y-2 hover:bg-accent rounded-md p-2 lg:p-3 -mx-2 lg:-mx-3 transition-colors my-0"
|
||||
>
|
||||
<div className="flex items-center gap-2 lg:gap-3">
|
||||
<item.icon className="w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
<span className="text-sm font-medium text-foreground group-hover:text-primary transition-colors">
|
||||
{item.title}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed ml-6 lg:ml-7">
|
||||
{item.description}
|
||||
</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as React from "react"
|
||||
|
||||
interface LogoProps extends React.SVGProps<SVGSVGElement> {
|
||||
size?: number
|
||||
}
|
||||
|
||||
export function Logo({ size = 24, className, ...props }: LogoProps) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M26 24.75C26.4142 24.75 26.75 24.4142 26.75 24C26.75 23.5858 26.4142 23.25 26 23.25V24.75ZM26 23.25H11V24.75H26V23.25ZM8.75 21V15H7.25V21H8.75ZM11 23.25C9.75736 23.25 8.75 22.2426 8.75 21H7.25C7.25 23.0711 8.92893 24.75 11 24.75V23.25Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M1.5 3.25C1.08579 3.25 0.75 3.58579 0.75 4C0.75 4.41421 1.08579 4.75 1.5 4.75V3.25ZM1.5 4.75H6V3.25H1.5V4.75ZM7.25 6V21H8.75V6H7.25ZM6 4.75C6.69036 4.75 7.25 5.30964 7.25 6H8.75C8.75 4.48122 7.51878 3.25 6 3.25V4.75Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M22 21.75C22.4142 21.75 22.75 21.4142 22.75 21C22.75 20.5858 22.4142 20.25 22 20.25V21.75ZM22 20.25H11V21.75H22V20.25ZM8.75 18V12H7.25V18H8.75ZM11 20.25C9.75736 20.25 8.75 19.2426 8.75 18H7.25C7.25 20.0711 8.92893 21.75 11 21.75V20.25Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M27.2057 19.754C27.0654 20.1438 26.6357 20.346 26.246 20.2057C25.8562 20.0654 25.654 19.6357 25.7943 19.246L27.2057 19.754ZM30.0361 9.67744L29.3305 9.4234L29.3305 9.4234L30.0361 9.67744ZM25.7943 19.246L29.3305 9.4234L30.7418 9.93148L27.2057 19.754L25.7943 19.246ZM28.1543 7.75L8 7.75V6.25L28.1543 6.25V7.75ZM29.3305 9.4234C29.6237 8.60882 29.0201 7.75 28.1543 7.75V6.25C30.059 6.25 31.3869 8.13941 30.7418 9.93148L29.3305 9.4234Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M13.5 21.75C13.0858 21.75 12.75 21.4142 12.75 21C12.75 20.5858 13.0858 20.25 13.5 20.25V21.75ZM26.7111 19.009L27.4174 19.2613L27.4174 19.2613L26.7111 19.009ZM13.5 20.25H23.8858V21.75H13.5V20.25ZM26.0048 18.7568L27.7937 13.7477L29.2063 14.2523L27.4174 19.2613L26.0048 18.7568ZM23.8858 20.25C24.8367 20.25 25.6849 19.6522 26.0048 18.7568L27.4174 19.2613C26.8843 20.7537 25.4706 21.75 23.8858 21.75V20.25Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M21.1694 10.5806L14.5651 17.1849" stroke="currentColor" />
|
||||
<path d="M22.1694 14.5806L18.5632 18.1868" stroke="currentColor" />
|
||||
<circle cx="13.1" cy="26.1" r="1.7" stroke="currentColor" />
|
||||
<circle cx="22.1" cy="26.1" r="1.7" stroke="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useTheme } from "@/hooks/use-theme"
|
||||
import { useCircularTransition } from "@/hooks/use-circular-transition"
|
||||
import "./theme-customizer/circular-transition.css"
|
||||
|
||||
interface ModeToggleProps {
|
||||
variant?: "outline" | "ghost" | "default"
|
||||
}
|
||||
|
||||
export function ModeToggle({ variant = "outline" }: ModeToggleProps) {
|
||||
const { theme } = useTheme()
|
||||
const { toggleTheme } = useCircularTransition()
|
||||
|
||||
// Simple, reliable dark mode detection with re-sync
|
||||
const [isDarkMode, setIsDarkMode] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
const updateMode = () => {
|
||||
if (theme === "dark") {
|
||||
setIsDarkMode(true)
|
||||
} else if (theme === "light") {
|
||||
setIsDarkMode(false)
|
||||
} else {
|
||||
setIsDarkMode(typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
}
|
||||
}
|
||||
|
||||
updateMode()
|
||||
|
||||
// Listen for system theme changes
|
||||
const mediaQuery = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null
|
||||
if (mediaQuery) {
|
||||
mediaQuery.addEventListener("change", updateMode)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (mediaQuery) {
|
||||
mediaQuery.removeEventListener("change", updateMode)
|
||||
}
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
const handleToggle = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
toggleTheme(event)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={variant}
|
||||
size="icon"
|
||||
onClick={handleToggle}
|
||||
className="cursor-pointer mode-toggle-button relative overflow-hidden"
|
||||
>
|
||||
{/* Show the icon for the mode you can switch TO */}
|
||||
{isDarkMode ? (
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] transition-transform duration-300 rotate-0 scale-100" />
|
||||
) : (
|
||||
<Moon className="h-[1.2rem] w-[1.2rem] transition-transform duration-300 rotate-0 scale-100" />
|
||||
)}
|
||||
<span className="sr-only">
|
||||
Switch to {isDarkMode ? "light" : "dark"} mode
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client"
|
||||
|
||||
import { ChevronRight, type LucideIcon } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible"
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
export function NavMain({
|
||||
label,
|
||||
items,
|
||||
}: {
|
||||
label: string
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon?: LucideIcon
|
||||
isActive?: boolean
|
||||
items?: {
|
||||
title: string
|
||||
url: string
|
||||
isActive?: boolean
|
||||
}[]
|
||||
}[]
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
|
||||
// Check if any subitem is active to determine if parent should be open
|
||||
const shouldBeOpen = (item: typeof items[0]) => {
|
||||
if (item.isActive) return true
|
||||
return item.items?.some(subItem => pathname === subItem.url) || false
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>{label}</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<Collapsible
|
||||
key={item.title}
|
||||
asChild
|
||||
defaultOpen={shouldBeOpen(item)}
|
||||
className="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
{item.items?.length ? (
|
||||
<>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton tooltip={item.title} className="cursor-pointer">
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
{item.items?.map((subItem) => (
|
||||
<SidebarMenuSubItem key={subItem.title}>
|
||||
<SidebarMenuSubButton asChild className="cursor-pointer" isActive={pathname === subItem.url}>
|
||||
<Link
|
||||
href={subItem.url}
|
||||
target={(item.title === "Auth Pages" || item.title === "Errors") ? "_blank" : undefined}
|
||||
rel={(item.title === "Auth Pages" || item.title === "Errors") ? "noopener noreferrer" : undefined}
|
||||
>
|
||||
<span>{subItem.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</>
|
||||
) : (
|
||||
<SidebarMenuButton asChild tooltip={item.title} className="cursor-pointer" isActive={pathname === item.url}>
|
||||
<Link href={item.url}>
|
||||
{item.icon && <item.icon />}
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as React from "react"
|
||||
import { type LucideIcon } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
export function NavSecondary({
|
||||
items,
|
||||
...props
|
||||
}: {
|
||||
items: {
|
||||
title: string
|
||||
url: string
|
||||
icon: LucideIcon
|
||||
}[]
|
||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
||||
return (
|
||||
<SidebarGroup {...props}>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton asChild size="sm" className="cursor-pointer">
|
||||
<Link href={item.url}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { useTransition } from "react";
|
||||
import {
|
||||
BellDot,
|
||||
CircleUser,
|
||||
CreditCard,
|
||||
EllipsisVertical,
|
||||
LogOut,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { signOutAction } from "@/lib/appwrite/auth-actions";
|
||||
|
||||
function initials(name: string) {
|
||||
const parts = name.trim().split(/\s+/).slice(0, 2);
|
||||
return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
|
||||
}
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
}: {
|
||||
user: { name: string; email: string };
|
||||
}) {
|
||||
const { isMobile } = useSidebar();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const handleSignOut = () => {
|
||||
startTransition(async () => {
|
||||
await signOutAction();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground cursor-pointer"
|
||||
>
|
||||
<div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg text-sm font-medium">
|
||||
{initials(user.name)}
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">{user.email}</span>
|
||||
</div>
|
||||
<EllipsisVertical className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg text-sm font-medium">
|
||||
{initials(user.name)}
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">{user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link href="/settings/account">
|
||||
<CircleUser />
|
||||
Profil
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link href="/settings/billing">
|
||||
<CreditCard />
|
||||
Plan & Faturalama
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link href="/settings/notifications">
|
||||
<BellDot />
|
||||
Bildirimler
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={handleSignOut}
|
||||
disabled={isPending}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<LogOut />
|
||||
{isPending ? "Çıkış yapılıyor..." : "Çıkış yap"}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Sparkles, Check } from "lucide-react"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface PricingPlan {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
price: string
|
||||
frequency: string
|
||||
features: string[]
|
||||
popular?: boolean
|
||||
current?: boolean
|
||||
}
|
||||
|
||||
interface PricingPlansProps {
|
||||
plans?: PricingPlan[]
|
||||
mode?: 'pricing' | 'billing'
|
||||
currentPlanId?: string
|
||||
onPlanSelect?: (planId: string) => void
|
||||
}
|
||||
|
||||
const defaultPlans: PricingPlan[] = [
|
||||
{
|
||||
id: 'basic',
|
||||
name: 'Basic',
|
||||
description: 'Perfect for small online stores',
|
||||
price: '$19',
|
||||
frequency: '/month',
|
||||
features: ['Up to 10 products', 'Basic inventory tracking', 'Email support', 'Mobile-responsive themes'],
|
||||
},
|
||||
{
|
||||
id: 'professional',
|
||||
name: 'Professional',
|
||||
description: 'Ideal for growing businesses',
|
||||
price: '$79',
|
||||
frequency: '/month',
|
||||
features: [
|
||||
'Up to 100 products',
|
||||
'Advanced analytics',
|
||||
'Priority email & chat support',
|
||||
'API access',
|
||||
'Custom domain',
|
||||
'Abandoned cart recovery',
|
||||
],
|
||||
popular: true,
|
||||
},
|
||||
{
|
||||
id: 'enterprise',
|
||||
name: 'Enterprise',
|
||||
description: 'For high-volume stores',
|
||||
price: '$199',
|
||||
frequency: '/month',
|
||||
features: [
|
||||
'Unlimited products',
|
||||
'Advanced reporting',
|
||||
'24/7 priority support',
|
||||
'Custom integrations',
|
||||
'Dedicated account manager',
|
||||
'Advanced security features',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export function PricingPlans({
|
||||
plans = defaultPlans,
|
||||
mode = 'pricing',
|
||||
currentPlanId,
|
||||
onPlanSelect
|
||||
}: PricingPlansProps) {
|
||||
const getButtonText = (plan: PricingPlan) => {
|
||||
if (mode === 'billing') {
|
||||
if (currentPlanId === plan.id) {
|
||||
return 'Current Plan'
|
||||
}
|
||||
const currentIndex = plans.findIndex(p => p.id === currentPlanId)
|
||||
const planIndex = plans.findIndex(p => p.id === plan.id)
|
||||
|
||||
if (planIndex > currentIndex) {
|
||||
return 'Upgrade Plan'
|
||||
} else if (planIndex < currentIndex) {
|
||||
return 'Downgrade Plan'
|
||||
}
|
||||
}
|
||||
return 'Get Started'
|
||||
}
|
||||
|
||||
const getButtonVariant = (plan: PricingPlan) => {
|
||||
if (mode === 'billing' && currentPlanId === plan.id) {
|
||||
return 'outline' as const
|
||||
}
|
||||
return plan.popular ? 'default' as const : 'outline' as const
|
||||
}
|
||||
|
||||
const isButtonDisabled = (plan: PricingPlan) => {
|
||||
return mode === 'billing' && currentPlanId === plan.id
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='grid gap-8 lg:grid-cols-3'>
|
||||
{plans.map(tier => (
|
||||
<Card
|
||||
key={tier.id}
|
||||
className={cn('flex flex-col pt-0', {
|
||||
'border-primary relative shadow-lg': tier.popular,
|
||||
'border-primary': currentPlanId === tier.id && mode === 'billing'
|
||||
})}
|
||||
aria-labelledby={`${tier.id}-title`}
|
||||
>
|
||||
{tier.popular && (
|
||||
<div className='absolute start-0 -top-3 w-full'>
|
||||
<Badge className='mx-auto flex w-fit gap-1.5 rounded-full font-medium'>
|
||||
<Sparkles className='!size-4' />
|
||||
{mode === 'pricing' && (
|
||||
<span>Most Popular</span>
|
||||
)}
|
||||
{currentPlanId === tier.id && mode === 'billing' && (
|
||||
<span>Current Plan</span>
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<CardHeader className='space-y-2 pt-8 text-center'>
|
||||
<CardTitle id={`${tier.id}-title`} className='text-2xl'>
|
||||
{tier.name}
|
||||
</CardTitle>
|
||||
<p className='text-muted-foreground text-sm text-balance'>{tier.description}</p>
|
||||
</CardHeader>
|
||||
<CardContent className='flex flex-1 flex-col space-y-6'>
|
||||
<div className='flex items-baseline justify-center'>
|
||||
<span className='text-4xl font-bold'>{tier.price}</span>
|
||||
<span className='text-muted-foreground text-sm'>{tier.frequency}</span>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
{tier.features.map(feature => (
|
||||
<div key={feature} className='flex items-center gap-2'>
|
||||
<div className='bg-muted rounded-full p-1'>
|
||||
<Check className='size-3.5' />
|
||||
</div>
|
||||
<span className='text-sm'>{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
className='w-full cursor-pointer'
|
||||
size='lg'
|
||||
variant={getButtonVariant(tier)}
|
||||
disabled={isButtonDisabled(tier)}
|
||||
onClick={() => onPlanSelect?.(tier.id)}
|
||||
aria-label={`${getButtonText(tier)} - ${tier.name} plan`}
|
||||
>
|
||||
{getButtonText(tier)}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { X } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Logo } from "./logo"
|
||||
|
||||
export function SidebarNotification() {
|
||||
const [isVisible, setIsVisible] = React.useState(true)
|
||||
|
||||
if (!isVisible) return null
|
||||
|
||||
return (
|
||||
<Card className="mb-3 py-0 border-neutral-200 bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-800">
|
||||
<CardContent className="p-4 relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute top-2 right-2 h-6 w-6 p-0 hover:bg-neutral-200 dark:hover:bg-neutral-700"
|
||||
onClick={() => setIsVisible(false)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
<span className="sr-only">Close notification</span>
|
||||
</Button>
|
||||
|
||||
<div className="pr-6">
|
||||
<h3 className="flex items-center gap-3 font-semibold text-neutral-900 dark:text-neutral-100 mb-2 mt-1">
|
||||
<Logo size={42} className="-mt-1" />
|
||||
<div>
|
||||
Welcome to{" "}
|
||||
<a
|
||||
href="https://shadcnstore.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
ShadcnStore
|
||||
</a>
|
||||
</div>
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground dark:text-neutral-400 leading-relaxed">
|
||||
Explore our premium Shadcn UI{" "}
|
||||
<a
|
||||
href="https://shadcnstore.com/blocks"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline"
|
||||
>
|
||||
blocks
|
||||
</a>{" "}
|
||||
to build your next project faster.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import Link from "next/link"
|
||||
|
||||
export function SiteFooter() {
|
||||
const year = new Date().getFullYear()
|
||||
return (
|
||||
<footer className="bg-background border-t">
|
||||
<div className="px-4 py-4 lg:px-6">
|
||||
<div className="text-muted-foreground flex flex-col items-center justify-between gap-2 text-xs sm:flex-row">
|
||||
<p>
|
||||
© {year} İşletmem — bir{" "}
|
||||
<Link
|
||||
href="https://kovaksoft.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-foreground hover:text-primary font-medium transition-colors"
|
||||
>
|
||||
KovakSoft
|
||||
</Link>{" "}
|
||||
ürünüdür.
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="#" className="hover:text-foreground transition-colors">
|
||||
Kullanım şartları
|
||||
</Link>
|
||||
<span aria-hidden>·</span>
|
||||
<Link href="#" className="hover:text-foreground transition-colors">
|
||||
Gizlilik
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Building2 } from "lucide-react";
|
||||
|
||||
import { CommandSearch, SearchTrigger } from "@/components/command-search";
|
||||
import { ModeToggle } from "@/components/mode-toggle";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
|
||||
import type { ShellCompany } from "@/app/(dashboard)/dashboard-shell";
|
||||
|
||||
export function SiteHeader({ company }: { company?: ShellCompany }) {
|
||||
const [searchOpen, setSearchOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
setSearchOpen((open) => !open);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", down);
|
||||
return () => document.removeEventListener("keydown", down);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 py-3 lg:gap-2 lg:px-6">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
|
||||
{company && (
|
||||
<div className="text-muted-foreground hidden items-center gap-1.5 text-sm md:flex">
|
||||
<Building2 className="size-3.5" />
|
||||
<span className="max-w-[260px] truncate">{company.name}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<div className="hidden w-full max-w-sm md:block">
|
||||
<SearchTrigger onClick={() => setSearchOpen(true)} />
|
||||
</div>
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<CommandSearch open={searchOpen} onOpenChange={setSearchOpen} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client"
|
||||
// Re-export the main components from the modular structure
|
||||
export { ThemeCustomizer, ThemeCustomizerTrigger } from './theme-customizer/main'
|
||||
@@ -0,0 +1,115 @@
|
||||
/* View Transition Circular Effect - Based on tweakcn implementation */
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
/* Ensure the outgoing view (old theme) is beneath */
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
/* Ensure the incoming view (new theme) is always on top */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@keyframes reveal {
|
||||
from {
|
||||
/* Use CSS variables for the origin, defaulting to center if not set */
|
||||
clip-path: circle(0% at var(--x, 50%) var(--y, 50%));
|
||||
opacity: 0.7;
|
||||
}
|
||||
to {
|
||||
/* Use CSS variables for the origin, defaulting to center if not set */
|
||||
clip-path: circle(150% at var(--x, 50%) var(--y, 50%));
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
/* Apply the reveal animation */
|
||||
animation: reveal 0.4s ease-in-out forwards;
|
||||
}
|
||||
|
||||
/* Fallback for reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Button styling for mode toggles */
|
||||
.mode-toggle-button {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.mode-toggle-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mode-toggle-button svg {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Enhanced mode toggle button with ripple effect */
|
||||
.mode-toggle-button {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.mode-toggle-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.1;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: all 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.mode-toggle-button.animating::before {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Improved focus and accessibility */
|
||||
.mode-toggle-button:focus-visible {
|
||||
outline: 2px solid var(--ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Support for reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.theme-transition-overlay.active,
|
||||
.theme-transition-overlay.reverse {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.mode-toggle-button::before {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode specific styling for the overlay */
|
||||
.dark .theme-transition-overlay {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
/* Light mode specific styling for the overlay */
|
||||
.light .theme-transition-overlay {
|
||||
background: var(--background);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
"use client"
|
||||
|
||||
import React from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import type { ImportedTheme } from '@/types/theme-customizer'
|
||||
|
||||
interface ImportModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onImport: (theme: ImportedTheme) => void
|
||||
}
|
||||
|
||||
export function ImportModal({ open, onOpenChange, onImport }: ImportModalProps) {
|
||||
const [importText, setImportText] = React.useState("")
|
||||
|
||||
const processImport = () => {
|
||||
try {
|
||||
if (!importText.trim()) {
|
||||
console.error("No CSS content provided")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse CSS content into light and dark theme variables
|
||||
const lightTheme: Record<string, string> = {}
|
||||
const darkTheme: Record<string, string> = {}
|
||||
|
||||
// Split CSS into sections
|
||||
const cssText = importText.replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
|
||||
|
||||
// Extract :root section (light theme)
|
||||
const rootMatch = cssText.match(/:root\s*\{([^}]+)\}/)
|
||||
if (rootMatch) {
|
||||
const rootContent = rootMatch[1]
|
||||
const variableMatches = rootContent.matchAll(/--([^:]+):\s*([^;]+);/g)
|
||||
for (const match of variableMatches) {
|
||||
const [, variable, value] = match
|
||||
lightTheme[variable.trim()] = value.trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Extract .dark section (dark theme)
|
||||
const darkMatch = cssText.match(/\.dark\s*\{([^}]+)\}/)
|
||||
if (darkMatch) {
|
||||
const darkContent = darkMatch[1]
|
||||
const variableMatches = darkContent.matchAll(/--([^:]+):\s*([^;]+);/g)
|
||||
for (const match of variableMatches) {
|
||||
const [, variable, value] = match
|
||||
darkTheme[variable.trim()] = value.trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Store the imported theme
|
||||
const importedThemeData = { light: lightTheme, dark: darkTheme }
|
||||
onImport(importedThemeData)
|
||||
|
||||
onOpenChange(false)
|
||||
setImportText("")
|
||||
} catch (error) {
|
||||
console.error("Error importing theme:", error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange} modal={true}>
|
||||
<DialogContent className="max-w-4xl w-[90vw]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tema CSS'i içe aktar</DialogTitle>
|
||||
<DialogDescription>
|
||||
CSS temasını aşağıya yapıştırın. Açık tema için <code>:root</code> ve koyu tema için{" "}
|
||||
<code>.dark</code> bölümleri olmalı; <code>--primary</code>, <code>--background</code> gibi CSS değişkenlerini içerir. Açık/koyu mod arasında otomatik geçiş yapılır.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Textarea
|
||||
id="theme-css"
|
||||
className="flex w-full rounded-md border border-input bg-transparent px-3 py-2 shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm max-h-[400px] min-h-[300px] font-mono text-sm text-foreground overflow-y-auto resize-none"
|
||||
placeholder={`:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: oklch(0.52 0.13 144.17);
|
||||
--primary: #3e2723;
|
||||
/* And more */
|
||||
}
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: hsl(37.50 36.36% 95.69%);
|
||||
--primary: rgb(46, 125, 50);
|
||||
/* And more */
|
||||
}`}
|
||||
value={importText}
|
||||
onChange={(e) => setImportText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} className="cursor-pointer">
|
||||
Vazgeç
|
||||
</Button>
|
||||
<Button onClick={processImport} disabled={!importText.trim()} className="cursor-pointer">
|
||||
İçe aktar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
"use client"
|
||||
|
||||
import React from 'react'
|
||||
import { Layout, Palette, RotateCcw, Settings, X } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { useThemeManager } from '@/hooks/use-theme-manager'
|
||||
import { useSidebarConfig } from '@/contexts/sidebar-context'
|
||||
import { tweakcnThemes } from '@/config/theme-data'
|
||||
import { ThemeTab } from './theme-tab'
|
||||
import { LayoutTab } from './layout-tab'
|
||||
import { ImportModal } from './import-modal'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { ImportedTheme } from '@/types/theme-customizer'
|
||||
|
||||
interface ThemeCustomizerProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function ThemeCustomizer({ open, onOpenChange }: ThemeCustomizerProps) {
|
||||
const { applyImportedTheme, isDarkMode, resetTheme, applyRadius, setBrandColorsValues, applyTheme, applyTweakcnTheme } = useThemeManager()
|
||||
const { config: sidebarConfig, updateConfig: updateSidebarConfig } = useSidebarConfig()
|
||||
|
||||
const [activeTab, setActiveTab] = React.useState("theme")
|
||||
const [selectedTheme, setSelectedTheme] = React.useState("default")
|
||||
const [selectedTweakcnTheme, setSelectedTweakcnTheme] = React.useState("")
|
||||
const [selectedRadius, setSelectedRadius] = React.useState("0.5rem")
|
||||
const [importModalOpen, setImportModalOpen] = React.useState(false)
|
||||
const [importedTheme, setImportedTheme] = React.useState<ImportedTheme | null>(null)
|
||||
|
||||
const handleReset = () => {
|
||||
// Complete reset to application defaults
|
||||
|
||||
// 1. Reset all state variables to initial values
|
||||
setSelectedTheme("default")
|
||||
setSelectedTweakcnTheme("")
|
||||
setSelectedRadius("0.5rem")
|
||||
setImportedTheme(null) // Clear imported theme
|
||||
setBrandColorsValues({}) // Clear brand colors state
|
||||
|
||||
// 2. Completely remove all custom CSS variables
|
||||
resetTheme()
|
||||
|
||||
// 3. Reset the radius to default
|
||||
applyRadius("0.5rem")
|
||||
|
||||
// 4. Reset sidebar to defaults
|
||||
updateSidebarConfig({ variant: "inset", collapsible: "offcanvas", side: "left" })
|
||||
}
|
||||
|
||||
const handleImport = (themeData: ImportedTheme) => {
|
||||
setImportedTheme(themeData)
|
||||
// Clear other selections to indicate custom import is active
|
||||
setSelectedTheme("")
|
||||
setSelectedTweakcnTheme("")
|
||||
|
||||
// Apply the imported theme
|
||||
applyImportedTheme(themeData, isDarkMode)
|
||||
}
|
||||
|
||||
const handleImportClick = () => {
|
||||
setImportModalOpen(true)
|
||||
}
|
||||
|
||||
// Re-apply themes when theme mode changes
|
||||
React.useEffect(() => {
|
||||
if (importedTheme) {
|
||||
applyImportedTheme(importedTheme, isDarkMode)
|
||||
} else if (selectedTheme) {
|
||||
applyTheme(selectedTheme, isDarkMode)
|
||||
} else if (selectedTweakcnTheme) {
|
||||
const selectedPreset = tweakcnThemes.find(t => t.value === selectedTweakcnTheme)?.preset
|
||||
if (selectedPreset) {
|
||||
applyTweakcnTheme(selectedPreset, isDarkMode)
|
||||
}
|
||||
}
|
||||
}, [isDarkMode, importedTheme, selectedTheme, selectedTweakcnTheme, applyImportedTheme, applyTheme, applyTweakcnTheme])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
|
||||
<SheetContent
|
||||
side={sidebarConfig.side === "left" ? "right" : "left"}
|
||||
className="w-[400px] p-0 gap-0 pointer-events-auto [&>button]:hidden overflow-hidden flex flex-col"
|
||||
onInteractOutside={(e) => {
|
||||
// Prevent the sheet from closing when dialog is open
|
||||
if (importModalOpen) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SheetHeader className="space-y-0 p-4 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<Settings className="h-4 w-4" />
|
||||
</div>
|
||||
<SheetTitle className="text-lg font-semibold">Görünüm</SheetTitle>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleReset}
|
||||
className="cursor-pointer h-8 w-8"
|
||||
aria-label="Varsayılana dön"
|
||||
title="Varsayılana dön"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="cursor-pointer h-8 w-8"
|
||||
aria-label="Kapat"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SheetDescription className="text-sm text-muted-foreground sr-only">
|
||||
Panelin renklerini ve düzenini özelleştirin.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="h-full flex flex-col">
|
||||
<div className="py-2">
|
||||
<TabsList className="grid w-full grid-cols-2 rounded-none h-12 p-1.5">
|
||||
<TabsTrigger value="theme" className="cursor-pointer data-[state=active]:bg-background">
|
||||
<Palette className="h-4 w-4 mr-1" /> Renk
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="layout" className="cursor-pointer data-[state=active]:bg-background">
|
||||
<Layout className="h-4 w-4 mr-1" /> Düzen
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="theme" className="flex-1 mt-0">
|
||||
<ThemeTab
|
||||
selectedTheme={selectedTheme}
|
||||
setSelectedTheme={setSelectedTheme}
|
||||
selectedTweakcnTheme={selectedTweakcnTheme}
|
||||
setSelectedTweakcnTheme={setSelectedTweakcnTheme}
|
||||
selectedRadius={selectedRadius}
|
||||
setSelectedRadius={setSelectedRadius}
|
||||
setImportedTheme={setImportedTheme}
|
||||
onImportClick={handleImportClick}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="layout" className="flex-1 mt-0">
|
||||
<LayoutTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<ImportModal
|
||||
open={importModalOpen}
|
||||
onOpenChange={setImportModalOpen}
|
||||
onImport={handleImport}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Floating trigger button - positioned dynamically based on sidebar side
|
||||
export function ThemeCustomizerTrigger({ onClick }: { onClick: () => void }) {
|
||||
const { config: sidebarConfig } = useSidebarConfig()
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
size="icon"
|
||||
className={cn(
|
||||
"fixed top-1/2 -translate-y-1/2 h-12 w-12 rounded-full shadow-lg z-50 bg-primary hover:bg-primary/90 text-primary-foreground cursor-pointer",
|
||||
sidebarConfig.side === "left" ? "right-4" : "left-4"
|
||||
)}
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
"use client"
|
||||
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { useSidebarConfig } from '@/contexts/sidebar-context'
|
||||
import { useSidebar } from '@/components/ui/sidebar'
|
||||
import { sidebarVariants, sidebarCollapsibleOptions, sidebarSideOptions } from '@/config/theme-customizer-constants'
|
||||
|
||||
export function LayoutTab() {
|
||||
const { config: sidebarConfig, updateConfig: updateSidebarConfig } = useSidebarConfig()
|
||||
const { toggleSidebar, state: sidebarState } = useSidebar()
|
||||
|
||||
// Sidebar handler functions
|
||||
const handleSidebarVariantSelect = (variant: "sidebar" | "floating" | "inset") => {
|
||||
updateSidebarConfig({ variant })
|
||||
}
|
||||
|
||||
const handleSidebarCollapsibleSelect = (collapsible: "offcanvas" | "icon" | "none") => {
|
||||
updateSidebarConfig({ collapsible })
|
||||
|
||||
// If switching to icon mode and sidebar is currently expanded, auto-collapse it
|
||||
if (collapsible === "icon" && sidebarState === "expanded") {
|
||||
toggleSidebar()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSidebarSideSelect = (side: "left" | "right") => {
|
||||
updateSidebarConfig({ side })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-6">
|
||||
{/* Kenar çubuğu ayarları */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Kenar çubuğu stili</Label>
|
||||
{sidebarConfig.variant && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{sidebarConfig.variant === "sidebar" && "Standart: klasik kenar çubuğu"}
|
||||
{sidebarConfig.variant === "floating" && "Yüzen: çerçeveli, yüzen kenar çubuğu"}
|
||||
{sidebarConfig.variant === "inset" && "İçeri çekik: yuvarlak köşeli içeri çekik"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{sidebarVariants.map((variant) => (
|
||||
<div
|
||||
key={variant.value}
|
||||
className={`relative p-4 border rounded-md cursor-pointer transition-colors ${
|
||||
sidebarConfig.variant === variant.value
|
||||
? "border-primary bg-primary/10"
|
||||
: "border-border hover:border-border/60"
|
||||
}`}
|
||||
onClick={() => handleSidebarVariantSelect(variant.value as "sidebar" | "floating" | "inset")}
|
||||
>
|
||||
{/* Visual representation of sidebar variant */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-semibold text-center">{variant.name}</div>
|
||||
<div className={`flex h-12 rounded border ${ variant.value === "inset" ? "bg-muted" : "bg-background" }`}>
|
||||
{/* Sidebar representation - smaller and more proportional */}
|
||||
<div
|
||||
className={`w-3 flex-shrink-0 bg-muted flex flex-col gap-0.5 p-1 ${
|
||||
variant.value === "floating" ? "border-r m-1 rounded" :
|
||||
variant.value === "inset" ? "m-1 ms-0 rounded bg-muted/80" :
|
||||
"border-r"
|
||||
}`}
|
||||
>
|
||||
{/* Menu icon representations - clearer and more visible */}
|
||||
<div className="h-0.5 w-full bg-foreground/60 rounded"></div>
|
||||
<div className="h-0.5 w-3/4 bg-foreground/50 rounded"></div>
|
||||
<div className="h-0.5 w-2/3 bg-foreground/40 rounded"></div>
|
||||
<div className="h-0.5 w-3/4 bg-foreground/30 rounded"></div>
|
||||
</div>
|
||||
{/* Main content area - larger and more prominent */}
|
||||
<div className={`flex-1 ${ variant.value === "inset" ? "bg-background ms-0" : "bg-background/50" } m-1 rounded-sm border-dashed border border-muted-foreground/20`}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Daraltma davranışı */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Daraltma davranışı</Label>
|
||||
{sidebarConfig.collapsible && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{sidebarConfig.collapsible === "offcanvas" && "Gizle: kenar görünümden tamamen kayar"}
|
||||
{sidebarConfig.collapsible === "icon" && "İkon: sadece ikonlar görünür"}
|
||||
{sidebarConfig.collapsible === "none" && "Sabit: her zaman açık"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{sidebarCollapsibleOptions.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={`relative p-4 border rounded-md cursor-pointer transition-colors ${
|
||||
sidebarConfig.collapsible === option.value
|
||||
? "border-primary bg-primary/10"
|
||||
: "border-border hover:border-border/60"
|
||||
}`}
|
||||
onClick={() => handleSidebarCollapsibleSelect(option.value as "offcanvas" | "icon" | "none")}
|
||||
>
|
||||
{/* Visual representation of collapsible mode */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-semibold text-center">{option.name}</div>
|
||||
<div className="flex h-12 rounded border bg-background">
|
||||
{/* Sidebar representation based on collapsible mode */}
|
||||
{option.value === "offcanvas" ? (
|
||||
// Off-canvas: Show collapsed state with hamburger menu
|
||||
<div className="flex-1 bg-background/50 m-1 rounded-sm border-dashed border border-muted-foreground/20 flex items-center justify-start pl-2">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="w-3 h-0.5 bg-foreground/60 rounded"></div>
|
||||
<div className="w-3 h-0.5 bg-foreground/60 rounded"></div>
|
||||
<div className="w-3 h-0.5 bg-foreground/60 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
) : option.value === "icon" ? (
|
||||
// Icon mode: Show thin icon sidebar with clear icons
|
||||
<>
|
||||
<div className="w-4 flex-shrink-0 bg-muted flex flex-col gap-1 p-1 border-r items-center">
|
||||
<div className="w-2 h-2 bg-foreground/60 rounded-sm"></div>
|
||||
<div className="w-2 h-2 bg-foreground/40 rounded-sm"></div>
|
||||
<div className="w-2 h-2 bg-foreground/30 rounded-sm"></div>
|
||||
</div>
|
||||
<div className="flex-1 bg-background/50 m-1 rounded-sm border-dashed border border-muted-foreground/20"></div>
|
||||
</>
|
||||
) : (
|
||||
// None: Always show full sidebar - more proportional
|
||||
<>
|
||||
<div className="w-6 flex-shrink-0 bg-muted flex flex-col gap-0.5 p-1 border-r">
|
||||
<div className="h-0.5 w-full bg-foreground/60 rounded"></div>
|
||||
<div className="h-0.5 w-3/4 bg-foreground/50 rounded"></div>
|
||||
<div className="h-0.5 w-2/3 bg-foreground/40 rounded"></div>
|
||||
<div className="h-0.5 w-3/4 bg-foreground/30 rounded"></div>
|
||||
</div>
|
||||
<div className="flex-1 bg-background/50 m-1 rounded-sm border-dashed border border-muted-foreground/20"></div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Kenar çubuğu konumu */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Kenar çubuğu konumu</Label>
|
||||
{sidebarConfig.side && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{sidebarConfig.side === "left" && "Sol: kenar çubuğu solda"}
|
||||
{sidebarConfig.side === "right" && "Sağ: kenar çubuğu sağda"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{sidebarSideOptions.map((side) => (
|
||||
<div
|
||||
key={side.value}
|
||||
className={`relative p-4 border rounded-md cursor-pointer transition-colors ${
|
||||
sidebarConfig.side === side.value
|
||||
? "border-primary bg-primary/10"
|
||||
: "border-border hover:border-border/60"
|
||||
}`}
|
||||
onClick={() => handleSidebarSideSelect(side.value as "left" | "right")}
|
||||
>
|
||||
{/* Visual representation of sidebar side */}
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-semibold text-center">{side.name}</div>
|
||||
<div className="flex h-12 rounded border bg-background">
|
||||
{side.value === "left" ? (
|
||||
// Left sidebar layout - more proportional
|
||||
<>
|
||||
<div className="w-6 flex-shrink-0 bg-muted flex flex-col gap-0.5 p-1 border-r">
|
||||
<div className="h-0.5 w-full bg-foreground/60 rounded"></div>
|
||||
<div className="h-0.5 w-3/4 bg-foreground/50 rounded"></div>
|
||||
<div className="h-0.5 w-2/3 bg-foreground/40 rounded"></div>
|
||||
<div className="h-0.5 w-3/4 bg-foreground/30 rounded"></div>
|
||||
</div>
|
||||
<div className="flex-1 bg-background/50 m-1 rounded-sm border-dashed border border-muted-foreground/20"></div>
|
||||
</>
|
||||
) : (
|
||||
// Right sidebar layout - more proportional
|
||||
<>
|
||||
<div className="flex-1 bg-background/50 m-1 rounded-sm border-dashed border border-muted-foreground/20"></div>
|
||||
<div className="w-6 flex-shrink-0 bg-muted flex flex-col gap-0.5 p-1 border-l">
|
||||
<div className="h-0.5 w-full bg-foreground/60 rounded"></div>
|
||||
<div className="h-0.5 w-3/4 bg-foreground/50 rounded"></div>
|
||||
<div className="h-0.5 w-2/3 bg-foreground/40 rounded"></div>
|
||||
<div className="h-0.5 w-3/4 bg-foreground/30 rounded"></div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Main theme customizer exports
|
||||
export { ThemeCustomizer, ThemeCustomizerTrigger } from './index'
|
||||
|
||||
// Individual component exports for flexibility
|
||||
export { ThemeTab } from './theme-tab'
|
||||
export { LayoutTab } from './layout-tab'
|
||||
export { ImportModal } from './import-modal'
|
||||
@@ -0,0 +1,290 @@
|
||||
"use client"
|
||||
|
||||
import { Dices, Upload, Sun, Moon } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
|
||||
import { useThemeManager } from '@/hooks/use-theme-manager'
|
||||
import { useCircularTransition } from '@/hooks/use-circular-transition'
|
||||
import { colorThemes, tweakcnThemes } from '@/config/theme-data'
|
||||
import { radiusOptions, baseColors } from '@/config/theme-customizer-constants'
|
||||
import { ColorPicker } from '@/components/color-picker'
|
||||
import type { ImportedTheme } from '@/types/theme-customizer'
|
||||
import React from 'react'
|
||||
import "./circular-transition.css"
|
||||
|
||||
interface ThemeTabProps {
|
||||
selectedTheme: string
|
||||
setSelectedTheme: (theme: string) => void
|
||||
selectedTweakcnTheme: string
|
||||
setSelectedTweakcnTheme: (theme: string) => void
|
||||
selectedRadius: string
|
||||
setSelectedRadius: (radius: string) => void
|
||||
setImportedTheme: (theme: ImportedTheme | null) => void
|
||||
onImportClick: () => void
|
||||
}
|
||||
|
||||
export function ThemeTab({
|
||||
selectedTheme,
|
||||
setSelectedTheme,
|
||||
selectedTweakcnTheme,
|
||||
setSelectedTweakcnTheme,
|
||||
selectedRadius,
|
||||
setSelectedRadius,
|
||||
setImportedTheme,
|
||||
onImportClick
|
||||
}: ThemeTabProps) {
|
||||
const {
|
||||
isDarkMode,
|
||||
brandColorsValues,
|
||||
setBrandColorsValues,
|
||||
applyTheme,
|
||||
applyTweakcnTheme,
|
||||
applyRadius,
|
||||
handleColorChange
|
||||
} = useThemeManager()
|
||||
|
||||
const { toggleTheme } = useCircularTransition()
|
||||
|
||||
const handleRandomShadcn = () => {
|
||||
// Apply a random shadcn theme
|
||||
const randomTheme = colorThemes[Math.floor(Math.random() * colorThemes.length)]
|
||||
setSelectedTheme(randomTheme.value)
|
||||
setSelectedTweakcnTheme("") // Clear tweakcn selection
|
||||
setBrandColorsValues({}) // Clear brand colors state
|
||||
setImportedTheme(null) // Clear imported theme
|
||||
applyTheme(randomTheme.value, isDarkMode)
|
||||
}
|
||||
|
||||
const handleRandomTweakcn = () => {
|
||||
// Apply a random tweakcn theme
|
||||
const randomTheme = tweakcnThemes[Math.floor(Math.random() * tweakcnThemes.length)]
|
||||
setSelectedTweakcnTheme(randomTheme.value)
|
||||
setSelectedTheme("") // Clear shadcn selection
|
||||
setBrandColorsValues({}) // Clear brand colors state
|
||||
setImportedTheme(null) // Clear imported theme
|
||||
applyTweakcnTheme(randomTheme.preset, isDarkMode)
|
||||
}
|
||||
|
||||
const handleRadiusSelect = (radius: string) => {
|
||||
setSelectedRadius(radius)
|
||||
applyRadius(radius)
|
||||
}
|
||||
|
||||
const handleLightMode = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (isDarkMode === false) return
|
||||
toggleTheme(event)
|
||||
}
|
||||
|
||||
const handleDarkMode = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (isDarkMode === true) return
|
||||
toggleTheme(event)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-6">
|
||||
|
||||
|
||||
{/* Hazır temalar */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Hazır temalar</Label>
|
||||
<Button variant="outline" size="sm" onClick={handleRandomShadcn} className="cursor-pointer">
|
||||
<Dices className="h-3.5 w-3.5 mr-1.5" />
|
||||
Rastgele
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Select value={selectedTheme} onValueChange={(value) => {
|
||||
setSelectedTheme(value)
|
||||
setSelectedTweakcnTheme("") // Clear other selection
|
||||
setBrandColorsValues({}) // Clear brand colors state
|
||||
setImportedTheme(null) // Clear imported theme
|
||||
applyTheme(value, isDarkMode)
|
||||
}}>
|
||||
<SelectTrigger className="w-full cursor-pointer">
|
||||
<SelectValue placeholder="Tema seçin" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-60">
|
||||
<div className="p-2">
|
||||
{colorThemes.map((theme) => (
|
||||
<SelectItem key={theme.value} value={theme.value} className="cursor-pointer">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-1">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.primary }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.secondary }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.accent }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.muted }}
|
||||
/>
|
||||
</div>
|
||||
<span>{theme.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Genişletilmiş temalar */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-sm font-medium">Genişletilmiş temalar</Label>
|
||||
<Button variant="outline" size="sm" onClick={handleRandomTweakcn} className="cursor-pointer">
|
||||
<Dices className="h-3.5 w-3.5 mr-1.5" />
|
||||
Rastgele
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Select value={selectedTweakcnTheme} onValueChange={(value) => {
|
||||
setSelectedTweakcnTheme(value)
|
||||
setSelectedTheme("") // Clear other selection
|
||||
setBrandColorsValues({}) // Clear brand colors state
|
||||
setImportedTheme(null) // Clear imported theme
|
||||
const selectedPreset = tweakcnThemes.find(t => t.value === value)?.preset
|
||||
if (selectedPreset) {
|
||||
applyTweakcnTheme(selectedPreset, isDarkMode)
|
||||
}
|
||||
}}>
|
||||
<SelectTrigger className="w-full cursor-pointer">
|
||||
<SelectValue placeholder="Tema seçin" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-60">
|
||||
<div className="p-2">
|
||||
{tweakcnThemes.map((theme) => (
|
||||
<SelectItem key={theme.value} value={theme.value} className="cursor-pointer">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-1">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.primary }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.secondary }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.accent }}
|
||||
/>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full border border-border/20"
|
||||
style={{ backgroundColor: theme.preset.styles.light.muted }}
|
||||
/>
|
||||
</div>
|
||||
<span>{theme.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Köşe yuvarlama */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Köşe yuvarlama</Label>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{radiusOptions.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={`relative cursor-pointer rounded-md p-3 border transition-colors ${
|
||||
selectedRadius === option.value
|
||||
? "border-primary"
|
||||
: "border-border hover:border-border/60"
|
||||
}`}
|
||||
onClick={() => handleRadiusSelect(option.value)}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="text-xs font-medium">{option.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Görünüm modu */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Görünüm modu</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant={!isDarkMode ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
onClick={handleLightMode}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Sun className="h-4 w-4 mr-1" />
|
||||
Açık
|
||||
</Button>
|
||||
<Button
|
||||
variant={isDarkMode ? "secondary" : "outline"}
|
||||
size="sm"
|
||||
onClick={handleDarkMode}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Moon className="h-4 w-4 mr-1" />
|
||||
Koyu
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Tema içe aktar */}
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={onImportClick}
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
<Upload className="h-3.5 w-3.5 mr-1.5" />
|
||||
Tema içe aktar
|
||||
</Button>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
tweakcn.com gibi araçlardan dışa aktardığınız JSON tema dosyasını yükleyebilirsiniz.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Marka renkleri */}
|
||||
<Accordion type="single" collapsible className="w-full border-b rounded-lg">
|
||||
<AccordionItem value="brand-colors" className="border border-border rounded-lg overflow-hidden">
|
||||
<AccordionTrigger className="px-4 py-3 hover:no-underline hover:bg-muted/50 transition-colors">
|
||||
<Label className="text-sm font-medium cursor-pointer">Marka renkleri</Label>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pb-4 pt-2 space-y-3 border-t border-border bg-muted/20">
|
||||
{baseColors.map((color) => (
|
||||
<div key={color.cssVar} className="flex items-center justify-between">
|
||||
<ColorPicker
|
||||
label={color.name}
|
||||
cssVar={color.cssVar}
|
||||
value={brandColorsValues[color.cssVar] || ""}
|
||||
onChange={handleColorChange}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProviderContext } from "@/contexts/theme-context"
|
||||
|
||||
type Theme = "dark" | "light" | "system"
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: React.ReactNode
|
||||
defaultTheme?: Theme
|
||||
storageKey?: string
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
defaultTheme = "system",
|
||||
storageKey = "vite-ui-theme",
|
||||
...props
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = React.useState<Theme>(
|
||||
() => (typeof window !== "undefined" && localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
const root = window.document.documentElement
|
||||
|
||||
root.classList.remove("light", "dark")
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light"
|
||||
|
||||
root.classList.add(systemTheme)
|
||||
return
|
||||
}
|
||||
|
||||
root.classList.add(theme)
|
||||
}, [theme])
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(storageKey, theme)
|
||||
}
|
||||
setTheme(theme)
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider {...props} value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,109 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,212 @@
|
||||
"use client"
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react"
|
||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
"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\_previous>svg]:rotate-180`,
|
||||
className
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString("default", { month: "short" }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"flex gap-4 flex-col md:flex-row relative",
|
||||
defaultClassNames.months
|
||||
),
|
||||
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||
defaultClassNames.nav
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn(
|
||||
"absolute bg-popover inset-0 opacity-0",
|
||||
defaultClassNames.dropdown
|
||||
),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
? "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",
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
table: "w-full border-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"select-none w-(--cell-size)",
|
||||
defaultClassNames.week_number_header
|
||||
),
|
||||
week_number: cn(
|
||||
"text-[0.8rem] select-none text-muted-foreground",
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
defaultClassNames.day
|
||||
),
|
||||
range_start: cn(
|
||||
"rounded-l-md bg-accent",
|
||||
defaultClassNames.range_start
|
||||
),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||
defaultClassNames.outside
|
||||
),
|
||||
disabled: cn(
|
||||
"text-muted-foreground opacity-50",
|
||||
defaultClassNames.disabled
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return (
|
||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
className={cn("size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus()
|
||||
}, [modifiers.focused])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
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",
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export const CardDecorator = ({ children }: { children: ReactNode }) => (
|
||||
<div className='relative mx-auto h-36 w-36'>
|
||||
{/* Light Mode Dot Pattern */}
|
||||
<div
|
||||
aria-hidden
|
||||
className='absolute inset-0 bg-[radial-gradient(circle,var(--color-foreground)_1px,transparent_1px)] bg-[length:16px_16px] opacity-30'
|
||||
/>
|
||||
{/* Light Mode Radial Fade */}
|
||||
<div aria-hidden className='to-card absolute inset-0 bg-radial from-transparent' />
|
||||
{/* Center Icon Container */}
|
||||
<div className='bg-background absolute inset-0 m-auto flex h-12 w-12 items-center justify-center rounded-md border shadow-xs'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
)
|
||||
}
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
id,
|
||||
className,
|
||||
children,
|
||||
config,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"]
|
||||
}) {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-slot="chart"
|
||||
data-chart={chartId}
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color
|
||||
)
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
|
||||
return (
|
||||
<div
|
||||
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",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"[&>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"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}) {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
|
||||
let configLabelKey: string = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config]
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
@@ -0,0 +1,182 @@
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn("overflow-hidden p-0", className)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"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=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
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
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-hidden select-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState } = useFormContext()
|
||||
const formState = useFormState({ name: fieldContext.name })
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
data-slot="form-control"
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message ?? "") : props.children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-message"
|
||||
id={formMessageId}
|
||||
className={cn("text-destructive text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function HoverCard({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
||||
}
|
||||
|
||||
function HoverCardTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function HoverCardContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
||||
<HoverCardPrimitive.Content
|
||||
data-slot="hover-card-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</HoverCardPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface LoadingSpinnerProps {
|
||||
className?: string
|
||||
size?: "sm" | "md" | "lg"
|
||||
}
|
||||
|
||||
export function LoadingSpinner({ className, size = "md" }: LoadingSpinnerProps) {
|
||||
const sizeClasses = {
|
||||
sm: "h-4 w-4",
|
||||
md: "h-8 w-8",
|
||||
lg: "h-12 w-12"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[200px]">
|
||||
<div
|
||||
className={cn(
|
||||
"animate-spin rounded-full border-b-2 border-primary",
|
||||
sizeClasses[size],
|
||||
className
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
children,
|
||||
viewport = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||
viewport?: boolean
|
||||
}) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Root
|
||||
data-slot="navigation-menu"
|
||||
data-viewport={viewport}
|
||||
className={cn(
|
||||
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{viewport && <NavigationMenuViewport />}
|
||||
</NavigationMenuPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.List
|
||||
data-slot="navigation-menu-list"
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Item
|
||||
data-slot="navigation-menu-item"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
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",
|
||||
"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
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuViewport({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||
)}
|
||||
>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
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)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuLink({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuIndicator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
navigationMenuTriggerStyle,
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="bg-primary h-full w-full flex-1 transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Progress }
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn("grid gap-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { GripVerticalIcon } from "lucide-react"
|
||||
import * as ResizablePrimitive from "react-resizable-panels"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ResizablePanelGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
|
||||
return (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
data-slot="resizable-panel-group"
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ResizablePanel({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
|
||||
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
|
||||
}
|
||||
|
||||
function ResizableHandle({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
withHandle?: boolean
|
||||
}) {
|
||||
return (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
data-slot="resizable-handle"
|
||||
className={cn(
|
||||
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
|
||||
<GripVerticalIcon className="size-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
)
|
||||
}
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
@@ -0,0 +1,183 @@
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
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 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" &&
|
||||
"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
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
@@ -0,0 +1,140 @@
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-dvh w-3/4 border-l sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-dvh w-3/4 border-r sm:max-w-sm",
|
||||
side === "top" &&
|
||||
"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" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn(
|
||||
"mt-auto flex flex-col gap-2 p-4 pb-[max(1rem,env(safe-area-inset-bottom))]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
@@ -0,0 +1,735 @@
|
||||
"use client"
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { PanelLeftIcon } from "lucide-react"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
openMobile: boolean
|
||||
setOpenMobile: (open: boolean) => void
|
||||
isMobile: boolean
|
||||
toggleSidebar: () => void
|
||||
}
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext)
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function SidebarProvider({
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) {
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||
const open = openProp ?? _open
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState)
|
||||
} else {
|
||||
_setOpen(openState)
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
},
|
||||
[setOpenProp, open]
|
||||
)
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||
}, [isMobile, setOpen, setOpenMobile])
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
return () => {
|
||||
if (typeof window !== "undefined") {
|
||||
window.removeEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
}
|
||||
}, [toggleSidebar])
|
||||
|
||||
// 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.
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
)
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
data-slot="sidebar-wrapper"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right"
|
||||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offcanvas" | "icon" | "none"
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group peer text-sidebar-foreground hidden md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
data-slot="sidebar"
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
data-slot="sidebar-gap"
|
||||
className={cn(
|
||||
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar-inner"
|
||||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarTrigger({
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-sidebar="trigger"
|
||||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("size-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
toggleSidebar()
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<button
|
||||
data-sidebar="rail"
|
||||
data-slot="sidebar-rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"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=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
return (
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Input>) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
className={cn("bg-background h-8 w-full shadow-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-header"
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-footer"
|
||||
data-sidebar="footer"
|
||||
className={cn(
|
||||
"flex flex-col gap-2 p-2",
|
||||
"[&>[data-slot=card]]:group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group"
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupLabel({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
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",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupAction({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu"
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-item"
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function SidebarMenuButton({
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
isActive?: boolean
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-button"
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!tooltip) {
|
||||
return button
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuAction({
|
||||
className,
|
||||
asChild = false,
|
||||
showOnHover = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
showOnHover?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSkeleton({
|
||||
className,
|
||||
showIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
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",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSubItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
data-sidebar="menu-sub-item"
|
||||
className={cn("group/menu-sub-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSubButton({
|
||||
asChild = false,
|
||||
size = "md",
|
||||
isActive = false,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
size?: "sm" | "md"
|
||||
isActive?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-sub-button"
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
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"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
@@ -0,0 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
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]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||
import { type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { toggleVariants } from "@/components/ui/toggle"
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
})
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ToggleGroupItem({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
const context = React.useContext(ToggleGroupContext)
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={context.variant || variant}
|
||||
data-size={context.size || size}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem }
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
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",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2 min-w-9",
|
||||
sm: "h-8 px-1.5 min-w-8",
|
||||
lg: "h-10 px-2.5 min-w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground 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
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { HoverCard, HoverCardTrigger, HoverCardContent } from "@/components/ui/hover-card"
|
||||
import { Rocket, Blocks, LayoutDashboard, ArrowRight } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
|
||||
|
||||
const SHADCN_BLOCKS_URL = "https://shadcnstore.com/blocks"
|
||||
|
||||
export function UpgradeToProButton() {
|
||||
|
||||
|
||||
return (
|
||||
<div className="fixed z-50 bottom-8 right-4 md:right-6 lg:right-8 flex flex-col items-end gap-2">
|
||||
<HoverCard openDelay={100} closeDelay={100}>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button
|
||||
size="lg"
|
||||
className="px-6 py-3 bg-gradient-to-br shadow-lg from-slate-900 cursor-pointer to-slate-400 text-white font-bold"
|
||||
style={{ minWidth: 180 }} onClick={() => typeof window !== "undefined" && window.open(SHADCN_BLOCKS_URL, "_blank")}
|
||||
>
|
||||
Upgrade to Pro
|
||||
<Rocket size={30} className="ml-1" />
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="mb-3 w-90 rounded-xl shadow-2xl bg-background border border-border p-3 animate-in fade-in slide-in-from-bottom-4 relative mr-4 md:mr-6 lg:mr-8">
|
||||
<div className="flex flex-col items-center text-center gap-3">
|
||||
<a href={SHADCN_BLOCKS_URL} target="_blank" rel="noopener noreferrer" className="cursor-pointer">
|
||||
<Image src="/hero-images-container.png" alt="shadcn" width={300} height={200} />
|
||||
</a>
|
||||
<h3 className="font-bold text-lg flex items-center py-2 gap-2">
|
||||
<Rocket size={18} className="text-primary" />
|
||||
Unlock Premium Blocks
|
||||
<Badge variant="destructive" className="text-xs px-2 py-0.5 rounded-full shadow">Live</Badge>
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm mb-4">
|
||||
Get access to exclusive premium blocks and dashboards for your next project. Elevate your UI instantly!
|
||||
</p>
|
||||
<div className="flex flex-row gap-2 w-full mt-2 justify-center">
|
||||
<div className="relative w-1/2">
|
||||
<a href={SHADCN_BLOCKS_URL} target="_blank" rel="noopener noreferrer">
|
||||
<Button className="w-full flex items-center justify-center cursor-pointer" variant="default">
|
||||
<Blocks size={16} />
|
||||
Pro Blocks
|
||||
<ArrowRight size={16} />
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
<div className="relative w-1/2">
|
||||
<Button className="w-full flex items-center justify-center" variant="default" disabled>
|
||||
<LayoutDashboard size={16} />
|
||||
Pro Dashboards
|
||||
</Button>
|
||||
<span className="absolute -top-5 -right-1">
|
||||
<Badge variant="outline" className="bg-yellow-400 text-yellow-900 border-yellow-400 text-xs px-2 py-0.5 rounded-full shadow">Coming soon</Badge>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user