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:
egecankomur
2026-05-05 04:37:04 +03:00
commit 37679e83e6
383 changed files with 53525 additions and 0 deletions
+191
View File
@@ -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>
);
}
+68
View File
@@ -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>
);
}
+81
View File
@@ -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>
)
}
+373
View File
@@ -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> &nbsp;·&nbsp; Gez &nbsp;·&nbsp; 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>
);
}
+125
View File
@@ -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
)}
/>
)
}
+13
View File
@@ -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" })
})
+80
View File
@@ -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
}
+87
View File
@@ -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>
)
}
+143
View File
@@ -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>
)
}
+44
View File
@@ -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>
)
}
+70
View File
@@ -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>
)
}
+101
View File
@@ -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>
)
}
+41
View File
@@ -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>
)
}
+120
View File
@@ -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>
);
}
+165
View File
@@ -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>
)
}
+58
View File
@@ -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>
)
}
+34
View File
@@ -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>
)
}
+56
View File
@@ -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} />
</>
);
}
+3
View File
@@ -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>
)
}
+186
View File
@@ -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>
)
}
+7
View File
@@ -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>
)
}
+59
View File
@@ -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>
)
}
+64
View File
@@ -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 }
+51
View File
@@ -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 }
+46
View File
@@ -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 }
+109
View File
@@ -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,
}
+59
View File
@@ -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 }
+212
View File
@@ -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 }
+17
View File
@@ -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>
)
+92
View File
@@ -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,
}
+353
View File
@@ -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,
}
+32
View File
@@ -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 }
+31
View File
@@ -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 }
+182
View File
@@ -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,
}
+143
View File
@@ -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,
}
+133
View File
@@ -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,
}
+255
View File
@@ -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,
}
+167
View File
@@ -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,
}
+42
View File
@@ -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 }
+21
View File
@@ -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 }
+22
View File
@@ -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 }
+28
View File
@@ -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>
)
}
+168
View File
@@ -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,
}
+48
View File
@@ -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 }
+29
View File
@@ -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 }
+43
View File
@@ -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 }
+56
View File
@@ -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 }
+56
View File
@@ -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 }
+183
View File
@@ -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,
}
+28
View File
@@ -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 }
+140
View File
@@ -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,
}
+735
View File
@@ -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,
}
+13
View File
@@ -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 }
+25
View File
@@ -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 }
+29
View File
@@ -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 }
+116
View File
@@ -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,
}
+64
View File
@@ -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 }
+18
View File
@@ -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 }
+73
View File
@@ -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 }
+45
View File
@@ -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 }
+59
View File
@@ -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 }
+66
View File
@@ -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>
)
}