feat(ui): complete phase 4 Poyraz UI foundation
This commit is contained in:
@@ -4,6 +4,7 @@ import type { ReactNode } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { Typography } from "poyraz-ui/atoms";
|
||||
import {
|
||||
ArrowUpRight,
|
||||
BarChart3,
|
||||
@@ -13,6 +14,11 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
type AuthPageShellProps = {
|
||||
branding: {
|
||||
applicationName: string;
|
||||
lightLogoUrl: string | null;
|
||||
darkLogoUrl: string | null;
|
||||
};
|
||||
title: string;
|
||||
description: string;
|
||||
imageSrc?: string;
|
||||
@@ -31,6 +37,7 @@ const highlights = [
|
||||
];
|
||||
|
||||
export function AuthPageShell({
|
||||
branding,
|
||||
title,
|
||||
description,
|
||||
form,
|
||||
@@ -63,8 +70,8 @@ export function AuthPageShell({
|
||||
<div className="absolute inset-0 opacity-20 bg-[linear-gradient(rgba(255,255,255,.18)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,.18)_1px,transparent_1px)] bg-size-[32px_32px]" />
|
||||
<div className="relative z-10 flex items-center gap-4 p-10">
|
||||
<Image
|
||||
src="/logo/lightLogoLong.png"
|
||||
alt="Neta"
|
||||
src={branding.darkLogoUrl ?? "/logo/lightLogoLong.png"}
|
||||
alt={branding.applicationName}
|
||||
width={240}
|
||||
height={64}
|
||||
className="h-16 w-auto object-contain"
|
||||
@@ -75,14 +82,18 @@ export function AuthPageShell({
|
||||
|
||||
<div className="relative z-10 px-10">
|
||||
<motion.div {...fadeUp}>
|
||||
<h1 className="max-w-2xl text-5xl font-semibold leading-[1.02] text-primary-foreground">
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="display"
|
||||
className="max-w-2xl text-5xl font-semibold leading-[1.02] text-primary-foreground"
|
||||
>
|
||||
Freelancer işlerini, müşterilerini ve finansını tek yerde yönet.
|
||||
</h1>
|
||||
<p className="mt-6 max-w-xl text-lg leading-8 text-primary-foreground/78">
|
||||
Neta, günlük operasyonunu, projelerini, side projectlerini ve
|
||||
</Typography>
|
||||
<Typography component="p" variant="lead" className="mt-6 max-w-xl text-lg leading-8 text-primary-foreground/78">
|
||||
{branding.applicationName}, günlük operasyonunu, projelerini, side projectlerini ve
|
||||
temel finans durumunu sade raporlarla takip etmen için
|
||||
tasarlanır.
|
||||
</p>
|
||||
</Typography>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
@@ -136,8 +147,8 @@ export function AuthPageShell({
|
||||
>
|
||||
<div className="mb-8 flex justify-center lg:hidden">
|
||||
<Image
|
||||
src="/logo/blackLogoLong.png"
|
||||
alt="Neta logo"
|
||||
src={branding.lightLogoUrl ?? "/logo/blackLogoLong.png"}
|
||||
alt={`${branding.applicationName} logo`}
|
||||
width={180}
|
||||
height={56}
|
||||
className="h-14 w-auto object-contain"
|
||||
@@ -147,10 +158,10 @@ export function AuthPageShell({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-center lg:text-left">
|
||||
<h2 className="text-3xl font-semibold tracking-normal text-foreground">
|
||||
<Typography component="h2" variant="h1" className="text-3xl font-semibold tracking-normal text-foreground">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-sm leading-6 text-muted-foreground">{description}</p>
|
||||
</Typography>
|
||||
<Typography component="p" variant="muted" className="text-sm leading-6">{description}</Typography>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 space-y-6">
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useFormStatus } from "react-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Button } from "poyraz-ui/atoms";
|
||||
import React from "react";
|
||||
|
||||
interface SubmitButtonProps extends React.ComponentProps<typeof Button> {
|
||||
@@ -18,10 +17,9 @@ export function SubmitButton({
|
||||
const { pending } = useFormStatus();
|
||||
|
||||
return (
|
||||
<Button type={type} disabled={pending} {...props}>
|
||||
<Button type={type} disabled={pending} loading={pending} aria-busy={pending} {...props}>
|
||||
{pending ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{pendingText || children}
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { showToast } from '@/components/ui/toast'
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "poyraz-ui/molecules";
|
||||
|
||||
export function ErrorToaster({ message }: { message: string }) {
|
||||
useEffect(() => {
|
||||
if (message) {
|
||||
showToast({ message, tone: 'error' })
|
||||
toast.error(message);
|
||||
}
|
||||
}, [message])
|
||||
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
+270
-260
@@ -1,16 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { signOut } from "@/app/login/actions";
|
||||
import { IconButton } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { PendingLink } from "@/components/ui/pending-link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronUp, LogOut, Menu, Settings } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
} from "poyraz-ui/atoms";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "poyraz-ui/molecules";
|
||||
import {
|
||||
SidebarBranding,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuItem,
|
||||
SidebarPanel,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarTrigger,
|
||||
SidebarUserProfile,
|
||||
useSidebar,
|
||||
} from "poyraz-ui/organisms";
|
||||
import { ChevronUp, LogOut, Settings } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export type AppShellNavItem = {
|
||||
title: string;
|
||||
@@ -23,6 +50,13 @@ export type AppShellNavGroup = {
|
||||
items: AppShellNavItem[];
|
||||
};
|
||||
|
||||
export type AppShellBranding = {
|
||||
applicationName: string;
|
||||
organizationName: string | null;
|
||||
lightLogoUrl: string | null;
|
||||
darkLogoUrl: string | null;
|
||||
};
|
||||
|
||||
type ShellUser = {
|
||||
email: string;
|
||||
displayName: string;
|
||||
@@ -31,6 +65,7 @@ type ShellUser = {
|
||||
};
|
||||
|
||||
type AppShellProps = {
|
||||
branding: AppShellBranding;
|
||||
children: React.ReactNode;
|
||||
homeHref: string;
|
||||
navGroups: AppShellNavGroup[];
|
||||
@@ -40,6 +75,7 @@ type AppShellProps = {
|
||||
};
|
||||
|
||||
export function AppShell({
|
||||
branding,
|
||||
children,
|
||||
homeHref,
|
||||
navGroups,
|
||||
@@ -48,173 +84,206 @@ export function AppShell({
|
||||
progress,
|
||||
}: AppShellProps) {
|
||||
const pathname = usePathname();
|
||||
const [mobileSidebarState, setMobileSidebarState] = useState({
|
||||
open: false,
|
||||
pathname,
|
||||
});
|
||||
const isMobileSidebarOpen =
|
||||
mobileSidebarState.open && mobileSidebarState.pathname === pathname;
|
||||
const sidebarProps = { branding, homeHref, navGroups, pathname, progress, settingsHref, user };
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[90] focus:rounded-md focus:bg-surface focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
Ana içeriğe geç
|
||||
</a>
|
||||
<div className="flex min-h-screen">
|
||||
<AppSidebar
|
||||
homeHref={homeHref}
|
||||
navGroups={navGroups}
|
||||
pathname={pathname}
|
||||
progress={progress}
|
||||
settingsHref={settingsHref}
|
||||
user={user}
|
||||
className="sticky top-0 hidden h-screen shrink-0 self-stretch lg:flex"
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[90] focus:rounded-md focus:bg-surface focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:shadow-lg focus:ring-2 focus:ring-focus-ring"
|
||||
>
|
||||
Ana içeriğe geç
|
||||
</a>
|
||||
|
||||
{isMobileSidebarOpen ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Menüyü kapat"
|
||||
className="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm lg:hidden"
|
||||
onClick={() => setMobileSidebarState({ open: false, pathname })}
|
||||
/>
|
||||
) : null}
|
||||
<div className="flex min-h-screen">
|
||||
<DesktopSidebar {...sidebarProps} />
|
||||
|
||||
<AppSidebar
|
||||
homeHref={homeHref}
|
||||
navGroups={navGroups}
|
||||
pathname={pathname}
|
||||
progress={progress}
|
||||
settingsHref={settingsHref}
|
||||
user={user}
|
||||
className={cn(
|
||||
"fixed inset-y-0 left-0 z-50 transition-transform duration-200 ease-out lg:hidden",
|
||||
isMobileSidebarOpen ? "translate-x-0" : "-translate-x-full",
|
||||
)}
|
||||
onNavigate={() => setMobileSidebarState({ open: false, pathname })}
|
||||
/>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<header className="sticky top-0 z-30 flex h-14 items-center justify-between border-b border-border bg-surface/95 px-4 backdrop-blur lg:hidden">
|
||||
<Link href={homeHref} className="flex items-center gap-2 font-semibold">
|
||||
<Image
|
||||
src="/logo/blackLogoLong.png"
|
||||
alt="Neta"
|
||||
width={120}
|
||||
height={32}
|
||||
className="h-8 w-auto object-contain"
|
||||
style={{ width: "auto" }}
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
<IconButton
|
||||
label="Menüyü aç"
|
||||
variant="outline"
|
||||
onClick={() => setMobileSidebarState({ open: true, pathname })}
|
||||
>
|
||||
<Menu className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</header>
|
||||
|
||||
<main id="main-content" className="min-w-0 flex-1 p-4 lg:p-8">
|
||||
{children}
|
||||
</main>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<MobileSidebar {...sidebarProps} />
|
||||
<main id="main-content" className="min-w-0 flex-1 p-4 lg:p-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppSidebar({
|
||||
homeHref,
|
||||
navGroups,
|
||||
pathname,
|
||||
progress,
|
||||
settingsHref,
|
||||
user,
|
||||
onNavigate,
|
||||
className,
|
||||
}: {
|
||||
type SidebarCompositionProps = {
|
||||
branding: AppShellBranding;
|
||||
homeHref: string;
|
||||
navGroups: AppShellNavGroup[];
|
||||
pathname: string;
|
||||
progress?: number;
|
||||
settingsHref: string;
|
||||
user: ShellUser;
|
||||
onNavigate?: () => void;
|
||||
className?: string;
|
||||
}) {
|
||||
};
|
||||
|
||||
function DesktopSidebar(props: SidebarCompositionProps) {
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
"flex h-dvh w-[280px] max-w-[82vw] flex-col overflow-hidden border-r border-border bg-surface",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="shrink-0 px-6 py-3">
|
||||
<Link href={homeHref} className="flex w-full items-center justify-center">
|
||||
<Image
|
||||
src="/logo/blackLogoLong.png"
|
||||
alt="Neta"
|
||||
width={160}
|
||||
height={48}
|
||||
className="h-12 w-auto object-contain"
|
||||
style={{ width: "auto" }}
|
||||
priority
|
||||
<SidebarProvider variant="collapsible">
|
||||
<SidebarPanel className="sticky top-0 hidden h-screen shrink-0 self-stretch lg:flex">
|
||||
<SidebarComposition {...props} />
|
||||
<SidebarRail aria-label="Kenar çubuğunu daralt veya genişlet" />
|
||||
</SidebarPanel>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileSidebar(props: SidebarCompositionProps) {
|
||||
return (
|
||||
<SidebarProvider variant="floating">
|
||||
<header className="sticky top-0 z-30 flex h-14 items-center justify-between border-b border-border bg-surface/95 px-4 backdrop-blur lg:hidden">
|
||||
<Link href={props.homeHref} className="min-w-0">
|
||||
<MobileBrand branding={props.branding} />
|
||||
</Link>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SidebarTrigger action="mobile" aria-label="Ana menüyü aç veya kapat" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Menü</TooltipContent>
|
||||
</Tooltip>
|
||||
</header>
|
||||
|
||||
<SidebarPanel className="h-dvh max-w-[82vw] lg:hidden">
|
||||
<SidebarComposition {...props} />
|
||||
</SidebarPanel>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarComposition({
|
||||
branding,
|
||||
homeHref,
|
||||
navGroups,
|
||||
pathname,
|
||||
progress,
|
||||
settingsHref,
|
||||
user,
|
||||
}: SidebarCompositionProps) {
|
||||
return (
|
||||
<>
|
||||
<SidebarHeader>
|
||||
<Link href={homeHref} className="min-w-0 flex-1" aria-label={`${branding.applicationName} ana sayfa`}>
|
||||
<SidebarBranding
|
||||
logo={<BrandMark branding={branding} />}
|
||||
title={branding.applicationName}
|
||||
subtitle={branding.organizationName ?? "Freelancer portalı"}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SidebarTrigger
|
||||
className="hidden lg:inline-flex"
|
||||
aria-label="Kenar çubuğunu daralt veya genişlet"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Kenar çubuğunu daralt</TooltipContent>
|
||||
</Tooltip>
|
||||
</SidebarHeader>
|
||||
|
||||
<div className="h-px bg-border" />
|
||||
<SidebarContent scrollMode="fade">
|
||||
<SidebarNavigation
|
||||
homeHref={homeHref}
|
||||
navGroups={navGroups}
|
||||
pathname={pathname}
|
||||
/>
|
||||
</SidebarContent>
|
||||
|
||||
<nav className="tiny-scrollbar min-h-0 flex-1 overflow-y-auto px-4 py-5">
|
||||
{navGroups.map((group, groupIndex) => (
|
||||
<section key={group.title} className={cn(groupIndex > 0 && "mt-6")}>
|
||||
<h2 className="px-2 text-[11px] font-semibold uppercase leading-6 text-muted-foreground">
|
||||
{group.title}
|
||||
</h2>
|
||||
<ul className="mt-2 space-y-1">
|
||||
{group.items.map((item) => {
|
||||
const isActive =
|
||||
item.href === homeHref
|
||||
? pathname === homeHref
|
||||
: item.href
|
||||
? pathname === item.href || pathname.startsWith(`${item.href}/`)
|
||||
: false;
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<li key={item.href || item.title}>
|
||||
<PendingLink
|
||||
href={item.href || "#"}
|
||||
onClick={onNavigate}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cn(
|
||||
"flex h-10 items-center gap-3 rounded-md px-3 text-sm font-medium text-muted-foreground transition-colors",
|
||||
"hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
isActive && "bg-primary/10 text-primary",
|
||||
)}
|
||||
showSpinner
|
||||
>
|
||||
{Icon ? <Icon className="h-4 w-4 shrink-0" /> : null}
|
||||
<span className="truncate">{item.title}</span>
|
||||
</PendingLink>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="mt-auto flex shrink-0 flex-col gap-4 border-t border-border px-4 py-4">
|
||||
<SidebarFooter className="flex flex-col gap-3">
|
||||
{typeof progress === "number" ? <ProgressSummary progress={progress} /> : null}
|
||||
<AccountMenu user={user} settingsHref={settingsHref} onNavigate={onNavigate} />
|
||||
</div>
|
||||
</aside>
|
||||
<AccountMenu user={user} settingsHref={settingsHref} />
|
||||
</SidebarFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarNavigation({
|
||||
homeHref,
|
||||
navGroups,
|
||||
pathname,
|
||||
}: Pick<SidebarCompositionProps, "homeHref" | "navGroups" | "pathname">) {
|
||||
const { setMobileOpen, variant } = useSidebar();
|
||||
|
||||
return navGroups.map((group) => (
|
||||
<SidebarGroup key={group.title}>
|
||||
<SidebarGroupLabel>{group.title}</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{group.items.map((item) => {
|
||||
const active =
|
||||
item.href === homeHref
|
||||
? pathname === homeHref
|
||||
: item.href
|
||||
? pathname === item.href || pathname.startsWith(`${item.href}/`)
|
||||
: false;
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<SidebarMenuItem
|
||||
key={item.href || item.title}
|
||||
href={item.href || "#"}
|
||||
active={active}
|
||||
icon={Icon ? <Icon className="h-4 w-4" aria-hidden="true" /> : undefined}
|
||||
onClick={() => {
|
||||
if (variant === "floating") setMobileOpen(false);
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
));
|
||||
}
|
||||
|
||||
function BrandMark({ branding }: { branding: AppShellBranding }) {
|
||||
const logoUrl = branding.lightLogoUrl ?? branding.darkLogoUrl;
|
||||
|
||||
if (!logoUrl) {
|
||||
return (
|
||||
<span className="flex h-full w-full items-center justify-center text-sm font-bold text-primary">
|
||||
{branding.applicationName.slice(0, 1).toLocaleUpperCase("tr-TR")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="flex h-full w-full items-center justify-center">
|
||||
<Image
|
||||
src={logoUrl}
|
||||
alt=""
|
||||
width={32}
|
||||
height={32}
|
||||
unoptimized
|
||||
className={branding.darkLogoUrl ? "h-full w-full object-contain dark:hidden" : "h-full w-full object-contain"}
|
||||
/>
|
||||
{branding.darkLogoUrl ? (
|
||||
<Image
|
||||
src={branding.darkLogoUrl}
|
||||
alt=""
|
||||
width={32}
|
||||
height={32}
|
||||
unoptimized
|
||||
className="hidden h-full w-full object-contain dark:block"
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileBrand({ branding }: { branding: AppShellBranding }) {
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-sm border border-border bg-muted">
|
||||
<BrandMark branding={branding} />
|
||||
</span>
|
||||
<Typography component="span" variant="small" className="truncate font-semibold">
|
||||
{branding.applicationName}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -222,16 +291,25 @@ function ProgressSummary({ progress }: { progress: number }) {
|
||||
const normalizedProgress = Math.max(0, Math.min(100, progress));
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden border-primary/10 bg-primary/5 shadow-none">
|
||||
<CardContent className="space-y-3 p-4">
|
||||
<div className="text-sm font-semibold text-foreground">Proje ilerlemesi</div>
|
||||
<Card variant="soft" className="w-full overflow-hidden border-primary/10 shadow-none">
|
||||
<CardContent className="space-y-3 p-3">
|
||||
<Typography variant="small" className="font-semibold">
|
||||
Proje ilerlemesi
|
||||
</Typography>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between text-xs font-medium">
|
||||
<span className="text-primary">%{normalizedProgress} tamamlandı</span>
|
||||
</div>
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-primary/10">
|
||||
<Typography variant="caption" className="font-medium text-primary">
|
||||
%{normalizedProgress} tamamlandı
|
||||
</Typography>
|
||||
<div
|
||||
role="progressbar"
|
||||
aria-label="Proje ilerlemesi"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={normalizedProgress}
|
||||
className="h-2 w-full overflow-hidden rounded-full bg-primary-muted"
|
||||
>
|
||||
<div
|
||||
className="h-full rounded-full bg-primary transition-[width] duration-700 ease-out"
|
||||
className="h-full rounded-full bg-primary transition-[width] duration-700 ease-out motion-reduce:transition-none"
|
||||
style={{ width: `${normalizedProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -241,110 +319,42 @@ function ProgressSummary({ progress }: { progress: number }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AccountMenu({
|
||||
user,
|
||||
settingsHref,
|
||||
onNavigate,
|
||||
}: {
|
||||
user: ShellUser;
|
||||
settingsHref: string;
|
||||
onNavigate?: () => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handlePointerDown(event: PointerEvent) {
|
||||
if (!menuRef.current?.contains(event.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
setOpen(false);
|
||||
buttonRef.current?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("pointerdown", handlePointerDown);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", handlePointerDown);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
function AccountMenu({ user, settingsHref }: { user: ShellUser; settingsHref: string }) {
|
||||
return (
|
||||
<div ref={menuRef} className="relative">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
aria-expanded={open}
|
||||
aria-haspopup="menu"
|
||||
className="flex w-full items-center gap-3 rounded-md p-2 text-left transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
>
|
||||
<UserAvatar user={user} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-foreground">{user.displayName}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{user.email}</div>
|
||||
</div>
|
||||
<ChevronUp className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
|
||||
{open ? (
|
||||
<div
|
||||
role="menu"
|
||||
className="absolute bottom-[calc(100%+0.5rem)] left-0 z-[70] w-full rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg"
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Hesap menüsünü aç"
|
||||
className="flex w-full items-center gap-2 rounded-sm p-1 text-left outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
>
|
||||
<PendingLink
|
||||
href={settingsHref}
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
onNavigate?.();
|
||||
}}
|
||||
className="flex h-9 items-center gap-2 rounded-sm px-3 text-sm hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
showSpinner
|
||||
<SidebarUserProfile
|
||||
className="min-w-0 flex-1"
|
||||
name={user.displayName}
|
||||
role={user.email}
|
||||
avatarUrl={user.avatarUrl ?? undefined}
|
||||
initials={user.shortName}
|
||||
/>
|
||||
<ChevronUp className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="top" className="min-w-52">
|
||||
<DropdownMenuItem asChild media={<Settings className="h-4 w-4" aria-hidden="true" />}>
|
||||
<Link href={settingsHref}>Ayarlar</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<form action={signOut}>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
media={<LogOut className="h-4 w-4" aria-hidden="true" />}
|
||||
className="text-destructive"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
Ayarlar
|
||||
</PendingLink>
|
||||
<div className="my-1 h-px bg-border" />
|
||||
<form action={signOut}>
|
||||
<button
|
||||
type="submit"
|
||||
role="menuitem"
|
||||
className="flex h-9 w-full items-center gap-2 rounded-sm px-3 text-left text-sm text-destructive hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
<button type="submit" className="w-full">
|
||||
Çıkış yap
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserAvatar({ user }: { user: ShellUser }) {
|
||||
if (user.avatarUrl) {
|
||||
return (
|
||||
<Image
|
||||
src={user.avatarUrl}
|
||||
alt=""
|
||||
width={36}
|
||||
height={36}
|
||||
className="h-9 w-9 rounded-full object-cover"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary">
|
||||
{user.shortName}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</form>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { AppShell } from "@/components/layout/app-shell";
|
||||
import { AppShell, type AppShellBranding } from "@/components/layout/app-shell";
|
||||
import { sidebarData } from "@/config/sidebar";
|
||||
|
||||
type DashboardShellProps = {
|
||||
branding: AppShellBranding;
|
||||
children: React.ReactNode;
|
||||
user: {
|
||||
email: string;
|
||||
@@ -13,9 +14,9 @@ type DashboardShellProps = {
|
||||
};
|
||||
};
|
||||
|
||||
export function DashboardShell({ children, user }: DashboardShellProps) {
|
||||
export function DashboardShell({ branding, children, user }: DashboardShellProps) {
|
||||
return (
|
||||
<AppShell homeHref="/" navGroups={sidebarData} settingsHref="/settings" user={user}>
|
||||
<AppShell branding={branding} homeHref="/" navGroups={sidebarData} settingsHref="/settings" user={user}>
|
||||
{children}
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { AppShell } from "@/components/layout/app-shell";
|
||||
import { AppShell, type AppShellBranding } from "@/components/layout/app-shell";
|
||||
import { portalSidebarData } from "@/config/portal-sidebar";
|
||||
|
||||
type PortalShellProps = {
|
||||
branding: AppShellBranding;
|
||||
children: React.ReactNode;
|
||||
user: {
|
||||
email: string;
|
||||
@@ -14,9 +15,10 @@ type PortalShellProps = {
|
||||
progress?: number;
|
||||
};
|
||||
|
||||
export function PortalShell({ children, user, progress }: PortalShellProps) {
|
||||
export function PortalShell({ branding, children, user, progress }: PortalShellProps) {
|
||||
return (
|
||||
<AppShell
|
||||
branding={branding}
|
||||
homeHref="/portal"
|
||||
navGroups={portalSidebarData}
|
||||
settingsHref="/portal/settings"
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "poyraz-ui/atoms";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "poyraz-ui/molecules";
|
||||
|
||||
type DestructiveConfirmationProps = {
|
||||
cancelLabel?: string;
|
||||
confirmLabel: string;
|
||||
description: string;
|
||||
loading?: boolean;
|
||||
onConfirm: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
open: boolean;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export function DestructiveConfirmation({
|
||||
cancelLabel = "Vazgeç",
|
||||
confirmLabel,
|
||||
description,
|
||||
loading = false,
|
||||
onConfirm,
|
||||
onOpenChange,
|
||||
open,
|
||||
title,
|
||||
}: DestructiveConfirmationProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent surface="solid" radius="lg" mobile="floating">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline" disabled={loading}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
loading={loading}
|
||||
aria-busy={loading}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Button, Card, CardContent, Skeleton, Typography } from "poyraz-ui/atoms";
|
||||
import { Alert, AlertDescription, AlertTitle } from "poyraz-ui/molecules";
|
||||
import { Ban, CircleAlert, Inbox } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type FeedbackStateProps = {
|
||||
action?: ReactNode;
|
||||
description: string;
|
||||
title: string;
|
||||
variant: "empty" | "error" | "forbidden";
|
||||
};
|
||||
|
||||
export function FeedbackState({ action, description, title, variant }: FeedbackStateProps) {
|
||||
if (variant === "empty") {
|
||||
return (
|
||||
<Card variant="soft">
|
||||
<CardContent className="flex min-h-56 flex-col items-center justify-center gap-3 p-8 text-center">
|
||||
<span className="flex h-11 w-11 items-center justify-center rounded-full bg-primary-muted text-primary-muted-foreground">
|
||||
<Inbox className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
<div className="space-y-1">
|
||||
<Typography component="h2" variant="h4">{title}</Typography>
|
||||
<Typography component="p" variant="muted" className="max-w-lg">{description}</Typography>
|
||||
</div>
|
||||
{action}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const forbidden = variant === "forbidden";
|
||||
return (
|
||||
<Alert
|
||||
role="alert"
|
||||
variant={forbidden ? "warning" : "destructive"}
|
||||
appearance="soft"
|
||||
icon={forbidden ? <Ban aria-hidden="true" /> : <CircleAlert aria-hidden="true" />}
|
||||
>
|
||||
<AlertTitle>{title}</AlertTitle>
|
||||
<AlertDescription className="space-y-3">
|
||||
<p>{description}</p>
|
||||
{action}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingState({ label = "İçerik yükleniyor" }: { label?: string }) {
|
||||
return (
|
||||
<div className="space-y-4" aria-busy="true" aria-label={label} role="status">
|
||||
<span className="sr-only">{label}</span>
|
||||
<Skeleton className="h-8 w-2/5" />
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<Skeleton key={index} className="h-36 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RetryAction({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<Button type="button" variant="outline" size="sm" onClick={onClick}>
|
||||
Yeniden dene
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Typography } from "poyraz-ui/atoms";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type PageHeaderProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
eyebrow?: string;
|
||||
primaryAction?: ReactNode;
|
||||
secondaryActions?: ReactNode;
|
||||
};
|
||||
|
||||
export function PageHeader({
|
||||
title,
|
||||
description,
|
||||
eyebrow,
|
||||
primaryAction,
|
||||
secondaryActions,
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<header className="flex flex-col gap-4 border-b border-border pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
{eyebrow ? (
|
||||
<Typography component="p" variant="caption" className="font-semibold uppercase tracking-wider text-primary">
|
||||
{eyebrow}
|
||||
</Typography>
|
||||
) : null}
|
||||
<Typography component="h1" variant="h1" balance>
|
||||
{title}
|
||||
</Typography>
|
||||
{description ? (
|
||||
<Typography component="p" variant="muted" className="max-w-3xl">
|
||||
{description}
|
||||
</Typography>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{primaryAction || secondaryActions ? (
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-2">
|
||||
{secondaryActions}
|
||||
{primaryAction}
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Badge } from "poyraz-ui/atoms";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
const statusPresentation = {
|
||||
accepted: { label: "Kabul edildi", variant: "success" },
|
||||
active: { label: "Aktif", variant: "success" },
|
||||
archived: { label: "Arşivlendi", variant: "outline" },
|
||||
cancelled: { label: "İptal edildi", variant: "outline" },
|
||||
completed: { label: "Tamamlandı", variant: "success" },
|
||||
done: { label: "Tamamlandı", variant: "success" },
|
||||
draft: { label: "Taslak", variant: "secondary" },
|
||||
expired: { label: "Süresi doldu", variant: "destructive" },
|
||||
in_progress: { label: "Devam ediyor", variant: "info" },
|
||||
overdue: { label: "Gecikmiş", variant: "destructive" },
|
||||
paid: { label: "Ödendi", variant: "success" },
|
||||
paused: { label: "Duraklatıldı", variant: "warning" },
|
||||
pending: { label: "Bekliyor", variant: "warning" },
|
||||
planned: { label: "Planlandı", variant: "secondary" },
|
||||
planning: { label: "Planlanıyor", variant: "secondary" },
|
||||
rejected: { label: "Reddedildi", variant: "destructive" },
|
||||
revoked: { label: "İptal edildi", variant: "destructive" },
|
||||
sent: { label: "Gönderildi", variant: "info" },
|
||||
todo: { label: "Yapılacak", variant: "secondary" },
|
||||
} as const satisfies Record<string, { label: string; variant: NonNullable<ComponentProps<typeof Badge>["variant"]> }>;
|
||||
|
||||
export type NetaStatus = keyof typeof statusPresentation;
|
||||
|
||||
type StatusBadgeProps = Omit<ComponentProps<typeof Badge>, "children" | "variant"> & {
|
||||
status: NetaStatus;
|
||||
};
|
||||
|
||||
export function StatusBadge({ status, ...props }: StatusBadgeProps) {
|
||||
const presentation = statusPresentation[status];
|
||||
return (
|
||||
<Badge variant={presentation.variant} {...props}>
|
||||
{presentation.label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export { statusPresentation };
|
||||
@@ -1,11 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef } from "react";
|
||||
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "danger";
|
||||
type ButtonSize = "sm" | "md" | "lg" | "icon" | "icon-sm";
|
||||
|
||||
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
|
||||
const variantClasses: Record<ButtonVariant, string> = {
|
||||
primary:
|
||||
"border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary-hover active:bg-primary-pressed",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-accent active:bg-accent-hover",
|
||||
outline:
|
||||
"border-border-strong bg-surface text-foreground shadow-sm hover:bg-accent active:bg-accent-hover",
|
||||
ghost:
|
||||
"border-transparent bg-transparent text-foreground hover:bg-accent active:bg-accent-hover",
|
||||
danger:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive-hover active:bg-destructive-pressed",
|
||||
};
|
||||
|
||||
const sizeClasses: Record<ButtonSize, string> = {
|
||||
sm: "h-8 gap-1.5 px-3 text-xs",
|
||||
md: "h-10 gap-2 px-4 text-sm",
|
||||
lg: "h-11 gap-2.5 px-5 text-sm",
|
||||
icon: "h-9 w-9 p-0",
|
||||
"icon-sm": "h-8 w-8 p-0",
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = "primary", size = "md", type = "button", ...props }, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
type={type}
|
||||
className={cn(
|
||||
"inline-flex shrink-0 items-center justify-center rounded-md border font-medium transition-colors",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
||||
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
variantClasses[variant],
|
||||
sizeClasses[size],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
|
||||
export type IconButtonProps = Omit<ButtonProps, "children" | "size"> & {
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
({ label, tooltip, children, className, ...props }, ref) => (
|
||||
<Button
|
||||
ref={ref}
|
||||
size="icon"
|
||||
aria-label={label}
|
||||
title={tooltip ?? label}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
),
|
||||
);
|
||||
|
||||
IconButton.displayName = "IconButton";
|
||||
@@ -1,28 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Card({ className, ...props }: ComponentPropsWithoutRef<"div">) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-md border border-border bg-card text-card-foreground shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardHeader({ className, ...props }: ComponentPropsWithoutRef<"div">) {
|
||||
return <div className={cn("space-y-1.5 p-5", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardTitle({ className, ...props }: ComponentPropsWithoutRef<"h3">) {
|
||||
return <h3 className={cn("text-base font-semibold", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardContent({ className, ...props }: ComponentPropsWithoutRef<"div">) {
|
||||
return <div className={cn("p-5 pt-0", className)} {...props} />;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Checkbox as CheckboxPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
|
||||
>
|
||||
<CheckIcon
|
||||
/>
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
@@ -1,168 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
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(
|
||||
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close data-slot="dialog-close" asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2"
|
||||
size="icon-sm"
|
||||
>
|
||||
<XIcon
|
||||
/>
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</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", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn(
|
||||
"font-heading text-base leading-none font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn(
|
||||
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
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,
|
||||
align = "start",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
align={align}
|
||||
className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 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 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", 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(
|
||||
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 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 right-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon
|
||||
/>
|
||||
</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,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 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 right-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon
|
||||
/>
|
||||
</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-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
|
||||
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(
|
||||
"flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn("z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Label({ className, ...props }: ComponentPropsWithoutRef<"label">) {
|
||||
return (
|
||||
<label
|
||||
className={cn("text-sm font-medium leading-none text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type FieldProps = ComponentPropsWithoutRef<"div"> & {
|
||||
label?: ReactNode;
|
||||
description?: ReactNode;
|
||||
error?: ReactNode;
|
||||
htmlFor?: string;
|
||||
};
|
||||
|
||||
export function Field({
|
||||
label,
|
||||
description,
|
||||
error,
|
||||
htmlFor,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: FieldProps) {
|
||||
const descriptionId = htmlFor && description ? `${htmlFor}-description` : undefined;
|
||||
const errorId = htmlFor && error ? `${htmlFor}-error` : undefined;
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-2", className)} {...props}>
|
||||
{label ? <Label htmlFor={htmlFor}>{label}</Label> : null}
|
||||
{children}
|
||||
{description ? (
|
||||
<p id={descriptionId} className="text-xs leading-5 text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
) : null}
|
||||
{error ? (
|
||||
<p id={errorId} role="alert" className="text-xs leading-5 text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
"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,
|
||||
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 | null>(null)
|
||||
|
||||
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, formState } = useFormContext()
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
if (!itemContext) {
|
||||
throw new Error("useFormField should be used within <FormItem>")
|
||||
}
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
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 | null>(null)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-[0.8rem] text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message ?? "") : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-[0.8rem] font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Icon as IconifyIcon } from '@iconify/react';
|
||||
|
||||
export function Icon({ icon, className }: { icon: string; className?: string }) {
|
||||
return <IconifyIcon icon={icon} className={className} />;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef } from "react";
|
||||
import type { InputHTMLAttributes, TextareaHTMLAttributes } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const controlClasses =
|
||||
"w-full rounded-md border border-input bg-input-bg px-3 text-sm text-foreground shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:opacity-80 aria-invalid:border-destructive aria-invalid:ring-destructive";
|
||||
|
||||
export type InputProps = InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => (
|
||||
<input ref={ref} className={cn(controlClasses, "h-10", className)} {...props} />
|
||||
));
|
||||
|
||||
Input.displayName = "Input";
|
||||
|
||||
export type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<textarea
|
||||
ref={ref}
|
||||
className={cn(controlClasses, "min-h-24 py-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
|
||||
Textarea.displayName = "Textarea";
|
||||
@@ -1,24 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Label as LabelPrimitive } from "radix-ui"
|
||||
|
||||
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 }
|
||||
@@ -1,26 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { WifiOff } from "lucide-react";
|
||||
|
||||
export function OfflineIndicator() {
|
||||
const [isOnline, setIsOnline] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Sadece client-side'da çalışır
|
||||
setIsOnline(navigator.onLine);
|
||||
|
||||
const handleOnline = () => setIsOnline(true);
|
||||
const handleOffline = () => setIsOnline(false);
|
||||
|
||||
window.addEventListener("online", handleOnline);
|
||||
window.addEventListener("offline", handleOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("online", handleOnline);
|
||||
window.removeEventListener("offline", handleOffline);
|
||||
};
|
||||
}, []);
|
||||
const isOnline = useSyncExternalStore(
|
||||
(onStoreChange) => {
|
||||
window.addEventListener("online", onStoreChange);
|
||||
window.addEventListener("offline", onStoreChange);
|
||||
return () => {
|
||||
window.removeEventListener("online", onStoreChange);
|
||||
window.removeEventListener("offline", onStoreChange);
|
||||
};
|
||||
},
|
||||
() => navigator.onLine,
|
||||
() => true,
|
||||
);
|
||||
|
||||
if (isOnline) return null;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Loader2 } from "lucide-react";
|
||||
import Link, { type LinkProps } from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
type AnchorHTMLAttributes,
|
||||
type MouseEvent,
|
||||
@@ -33,11 +32,8 @@ export function PendingLink({
|
||||
...props
|
||||
}: PendingLinkProps) {
|
||||
const pathname = usePathname();
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setPending(false);
|
||||
}, [pathname]);
|
||||
const [pendingFromPath, setPendingFromPath] = useState<string | null>(null);
|
||||
const pending = pendingFromPath === pathname;
|
||||
|
||||
function handleClick(event: MouseEvent<HTMLAnchorElement>) {
|
||||
onClick?.(event);
|
||||
@@ -58,7 +54,7 @@ export function PendingLink({
|
||||
const nextPath = hrefValue.split("?")[0].split("#")[0];
|
||||
|
||||
if (nextPath && nextPath !== pathname) {
|
||||
setPending(true);
|
||||
setPendingFromPath(pathname);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Button } from "poyraz-ui/atoms";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { useFormStatus } from "react-dom";
|
||||
|
||||
@@ -34,6 +34,7 @@ export function PendingSubmitButton({
|
||||
type={type}
|
||||
disabled={disabled || pending}
|
||||
aria-busy={pending}
|
||||
loading={pending && !pendingIcon}
|
||||
className={cn(className)}
|
||||
>
|
||||
{icon}
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Select as SelectPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return (
|
||||
<SelectPrimitive.Group
|
||||
data-slot="select-group"
|
||||
className={cn("scroll-my-1 p-1", className)}
|
||||
{...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(
|
||||
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "item-aligned",
|
||||
align = "center",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
data-align-trigger={position === "item-aligned"}
|
||||
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", 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}
|
||||
align={align}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
data-position={position}
|
||||
className={cn(
|
||||
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
|
||||
position === "popper" && ""
|
||||
)}
|
||||
>
|
||||
{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("px-1.5 py-1 text-xs text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground 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="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="pointer-events-none" />
|
||||
</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("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon
|
||||
/>
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon
|
||||
/>
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||
|
||||
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(
|
||||
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Skeleton({ className, ...props }: ComponentPropsWithoutRef<"div">) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -1,157 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { ComponentPropsWithoutRef, ReactElement } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { IconButton } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ToastTone = "success" | "error" | "info";
|
||||
|
||||
type ToastItem = {
|
||||
id: number;
|
||||
message: string;
|
||||
tone: ToastTone;
|
||||
};
|
||||
|
||||
type ToastPayload = {
|
||||
message: string;
|
||||
tone?: ToastTone;
|
||||
};
|
||||
|
||||
const toastEventName = "neta:toast";
|
||||
let toastId = 0;
|
||||
|
||||
export function showToast(payload: ToastPayload) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent<ToastPayload>(toastEventName, { detail: payload }));
|
||||
}
|
||||
|
||||
export function Toaster() {
|
||||
const [items, setItems] = useState<ToastItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleToast(event: Event) {
|
||||
const customEvent = event as CustomEvent<ToastPayload>;
|
||||
const item = {
|
||||
id: ++toastId,
|
||||
message: customEvent.detail.message,
|
||||
tone: customEvent.detail.tone ?? "info",
|
||||
};
|
||||
|
||||
setItems((current) => [...current.slice(-2), item]);
|
||||
window.setTimeout(() => {
|
||||
setItems((current) => current.filter((toast) => toast.id !== item.id));
|
||||
}, 4500);
|
||||
}
|
||||
|
||||
window.addEventListener(toastEventName, handleToast);
|
||||
return () => window.removeEventListener(toastEventName, handleToast);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-live="polite"
|
||||
aria-relevant="additions text"
|
||||
className="fixed bottom-4 right-4 z-[80] flex w-[min(calc(100vw-2rem),24rem)] flex-col gap-2"
|
||||
>
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"flex items-start gap-3 rounded-md border bg-surface p-4 text-sm shadow-lg",
|
||||
item.tone === "error" && "border-destructive/30",
|
||||
item.tone === "success" && "border-success/30",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-1 h-2 w-2 shrink-0 rounded-full bg-info",
|
||||
item.tone === "error" && "bg-destructive",
|
||||
item.tone === "success" && "bg-success",
|
||||
)}
|
||||
/>
|
||||
<p className="min-w-0 flex-1 text-foreground">{item.message}</p>
|
||||
<IconButton
|
||||
label="Bildirimi kapat"
|
||||
variant="ghost"
|
||||
className="-mr-2 -mt-2 h-8 w-8"
|
||||
onClick={() => setItems((current) => current.filter((toast) => toast.id !== item.id))}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type ToastProps = ComponentPropsWithoutRef<"div"> & {
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
variant?: "default" | "destructive";
|
||||
};
|
||||
|
||||
export type ToastActionElement = ReactElement;
|
||||
|
||||
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export function Toast({ open = true, onOpenChange, variant, className, ...props }: ToastProps) {
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role={variant === "destructive" ? "alert" : "status"}
|
||||
className={cn(
|
||||
"group pointer-events-auto flex w-full items-start gap-3 rounded-md border bg-surface p-4 text-sm shadow-lg",
|
||||
variant === "destructive" && "border-destructive/30",
|
||||
className,
|
||||
)}
|
||||
data-on-open-change={onOpenChange ? "" : undefined}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToastTitle({ className, ...props }: ComponentPropsWithoutRef<"div">) {
|
||||
return <div className={cn("font-medium text-foreground", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function ToastDescription({ className, ...props }: ComponentPropsWithoutRef<"div">) {
|
||||
return <div className={cn("text-sm text-muted-foreground", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function ToastClose({ className, ...props }: ComponentPropsWithoutRef<"button">) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"ml-auto rounded-sm p-1 text-muted-foreground opacity-80 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Bildirimi kapat</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToastViewport({ className, ...props }: ComponentPropsWithoutRef<"div">) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed bottom-4 right-4 z-[80] flex w-[min(calc(100vw-2rem),24rem)] flex-col gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user