feat: PL24 integration + EMEX on-demand parts loading

## Major Changes

### PL24 Integration (PartsLink24 API)
- Add PL24 auth service with JWT token management
- Add PL24 service for VIN decode and parts catalog
- Support for VAG, Mercedes, BMW, Ford, Renault and more
- Schema image download with base64 decoding
- Image deduplication via SchemaPic table

### EMEX On-Demand Loading
- Remove automatic parts scraping during VIN decode
- Add fetchCategoryParts() for on-demand loading
- Store category URLs in rawResponse for later fetching
- Fix Chrome executable path for Puppeteer

### VIN Decode Flow
- PL24 first, fallback to EMEX if not available
- Both sources now use on-demand parts loading
- Faster VIN lookup (no parts scraped upfront)

### Database Schema
- Add SchemaPic model for image deduplication
- Add schemaPicId to VehicleCategory
- Add EMEX models for parallel scraping

### Frontend Improvements
- Dark theme with Tailwind CSS
- Improved UI components (card, button, input)
- Better category and parts display
- Schema image viewer

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Semih
2026-01-22 11:15:06 +01:00
parent b9532a61ee
commit 2358b38b4d
155 changed files with 31575 additions and 1109 deletions

View File

@@ -3,21 +3,50 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'inline-flex items-center rounded-full text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
// Primary
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
'bg-primary text-primary-foreground ring ring-primary/20',
// Secondary
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
'bg-secondary text-secondary-foreground ring ring-border',
// Outline
outline:
'bg-transparent text-foreground ring ring-border',
// Destructive/Danger
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
'bg-destructive text-destructive-foreground ring ring-destructive/20',
// Success
success:
'bg-success text-success-foreground ring ring-success/20',
// Warning
warning:
'bg-warning text-warning-foreground ring ring-warning/20',
// Accent
accent:
'bg-accent text-accent-foreground ring ring-accent/20',
// Soft variants
'soft-primary':
'bg-primary-50 text-primary ring ring-primary/10',
'soft-destructive':
'bg-destructive-50 text-destructive ring ring-destructive/10',
'soft-success':
'bg-success-50 text-success ring ring-success/10',
'soft-warning':
'bg-warning-50 text-warning ring ring-warning/10',
},
size: {
sm: 'px-2 py-0.5 text-[10px]',
default: 'px-2.5 py-0.5',
lg: 'px-3 py-1 text-sm',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
@@ -26,9 +55,9 @@ export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
function Badge({ className, variant, size, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
<div className={cn(badgeVariants({ variant, size }), className)} {...props} />
);
}

View File

@@ -1,30 +1,76 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 active:scale-[0.98]',
'relative inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium select-none cursor-pointer transition-all focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm hover:shadow-md',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm hover:shadow-md',
outline: 'border-2 border-input bg-background hover:bg-accent hover:text-accent-foreground hover:border-accent',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
// Primary - Selia style with inset shadow
default:
'bg-primary text-primary-foreground ring ring-primary/20 shadow-sm inset-shadow-white/15 hover:bg-primary/90 active:scale-[0.98]',
// Secondary
secondary:
'bg-secondary text-secondary-foreground ring ring-border hover:bg-secondary/80 active:scale-[0.98]',
// Tertiary
tertiary:
'bg-tertiary text-tertiary-foreground ring ring-border hover:bg-tertiary/80 active:scale-[0.98]',
// Danger/Destructive
destructive:
'bg-destructive text-destructive-foreground ring ring-destructive/20 shadow-sm inset-shadow-white/15 hover:bg-destructive/90 active:scale-[0.98]',
// Outline - Ring based border
outline:
'bg-transparent ring ring-border text-foreground hover:bg-muted hover:ring-border-secondary active:bg-muted/80',
// Ghost
ghost:
'text-foreground hover:bg-muted active:bg-muted/80',
// Plain/Link
plain:
'text-foreground hover:text-primary underline-offset-4 hover:underline',
// Link
link:
'text-primary underline-offset-4 hover:underline',
// Gradient - Selia premium style
gradient:
'gradient-primary text-white shadow-md ring ring-primary/20 inset-shadow-white/15 hover:opacity-95 active:scale-[0.98]',
// Success
success:
'bg-success text-success-foreground ring ring-success/20 shadow-sm inset-shadow-white/15 hover:bg-success/90 active:scale-[0.98]',
// Warning
warning:
'bg-warning text-warning-foreground ring ring-warning/20 shadow-sm hover:bg-warning/90 active:scale-[0.98]',
},
size: {
default: 'h-10 px-5 py-2',
sm: 'h-9 rounded-lg px-4',
lg: 'h-12 rounded-xl px-8',
icon: 'h-10 w-10',
xs: 'h-7 px-2.5 text-xs rounded-md [&_svg]:size-3',
sm: 'h-8 px-3 text-sm rounded-lg [&_svg]:size-3.5',
default: 'h-9 px-4 text-sm rounded-lg [&_svg]:size-4',
md: 'h-10 px-5 text-sm rounded-lg [&_svg]:size-4',
lg: 'h-11 px-6 text-base rounded-xl [&_svg]:size-5',
xl: 'h-12 px-8 text-base font-semibold rounded-xl [&_svg]:size-5',
// Icon sizes
'icon-xs': 'h-7 w-7 rounded-md [&_svg]:size-3.5',
'icon-sm': 'h-8 w-8 rounded-lg [&_svg]:size-4',
icon: 'h-9 w-9 rounded-lg [&_svg]:size-4',
'icon-md': 'h-10 w-10 rounded-lg [&_svg]:size-5',
'icon-lg': 'h-11 w-11 rounded-xl [&_svg]:size-5',
},
pill: {
true: 'rounded-full',
false: '',
},
block: {
true: 'w-full',
false: '',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
pill: false,
block: false,
},
},
);
@@ -33,12 +79,35 @@ export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
isLoading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
({ className, variant, size, pill, block, asChild = false, isLoading, leftIcon, rightIcon, children, disabled, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
return (
<Comp
className={cn(buttonVariants({ variant, size, pill, block, className }))}
ref={ref}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? (
<>
<Loader2 className="animate-spin" />
<span>{children}</span>
</>
) : (
<>
{leftIcon && <span className="inline-flex">{leftIcon}</span>}
{children}
{rightIcon && <span className="inline-flex">{rightIcon}</span>}
</>
)}
</Comp>
);
},
);
Button.displayName = 'Button';

View File

@@ -1,51 +1,134 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
const cardVariants = cva(
'rounded-xl bg-card text-card-foreground ring ring-card-border shadow-card transition-all',
{
variants: {
variant: {
default: '',
elevated: 'shadow-md hover:shadow-lg',
outline: 'bg-transparent shadow-none',
ghost: 'ring-0 bg-transparent shadow-none',
glass: 'glass ring-0',
gradient: 'gradient-border ring-0 bg-card',
interactive: 'cursor-pointer hover:ring-border-secondary hover:shadow-md',
success: 'ring-success/30 bg-success-50',
warning: 'ring-warning/30 bg-warning-50',
destructive: 'ring-destructive/30 bg-destructive-50',
},
padding: {
none: '',
sm: 'p-4',
default: 'p-6',
lg: 'p-8',
},
},
defaultVariants: {
variant: 'default',
padding: 'none',
},
},
);
export interface CardProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof cardVariants> {}
const Card = React.forwardRef<HTMLDivElement, CardProps>(
({ className, variant, padding, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-xl border bg-card text-card-foreground shadow-sm transition-all duration-300',
className
)}
className={cn(cardVariants({ variant, padding }), className)}
{...props}
/>
),
);
Card.displayName = 'Card';
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
),
);
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
compact?: boolean;
align?: 'default' | 'center' | 'right';
}
>(({ className, compact, align = 'default', ...props }, ref) => (
<div
ref={ref}
className={cn(
'flex flex-col border-b border-card-separator',
compact ? 'gap-y-1 p-4' : 'gap-y-1.5 p-6',
align === 'center' && 'items-center text-center',
align === 'right' && 'items-end text-right',
className,
)}
{...props}
/>
));
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} />
),
);
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & { as?: 'h1' | 'h2' | 'h3' | 'h4' }
>(({ className, as: Component = 'h3', ...props }, ref) => (
<Component
ref={ref as React.Ref<HTMLHeadingElement>}
className={cn(
'text-lg font-semibold leading-none tracking-tight text-foreground',
className,
)}
{...props}
/>
));
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
),
);
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />,
);
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & { compact?: boolean }
>(({ className, compact, ...props }, ref) => (
<div
ref={ref}
className={cn(compact ? 'p-4' : 'p-6', className)}
{...props}
/>
));
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
),
);
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & { compact?: boolean }
>(({ className, compact, ...props }, ref) => (
<div
ref={ref}
className={cn(
'flex items-center gap-1.5 border-t border-card-separator bg-card-footer rounded-b-xl',
compact ? 'p-4' : 'p-6',
className,
)}
{...props}
/>
));
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
cardVariants,
};

View File

@@ -1,23 +1,108 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
const inputVariants = cva(
'flex w-full rounded-lg text-sm transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-dimmed focus:outline-0 disabled:cursor-not-allowed disabled:opacity-50',
{
variants: {
variant: {
// Default - Selia ring-based border
default:
'bg-input ring ring-input-border hover:not-disabled:not-focus:ring-input-accent-border focus:ring-2 focus:ring-primary',
// Subtle - Transparent background
subtle:
'bg-input/60 ring ring-input-border hover:not-disabled:not-focus:ring-input-accent-border focus:ring-2 focus:ring-primary',
// Filled
filled:
'bg-secondary ring-0 focus:ring-2 focus:ring-primary',
// Ghost
ghost:
'bg-transparent ring-0 hover:bg-muted focus:ring-2 focus:ring-primary',
// Error
error:
'bg-destructive-50 ring ring-destructive/50 text-destructive focus:ring-2 focus:ring-destructive',
// Success
success:
'bg-success-50 ring ring-success/50 text-success focus:ring-2 focus:ring-success',
},
size: {
xs: 'h-7 px-2.5 text-xs',
sm: 'h-8 px-3 text-sm',
default: 'h-9 px-3.5',
md: 'h-10 px-4',
lg: 'h-11 px-4 text-base',
xl: 'h-12 px-5 text-base',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
export interface InputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>,
VariantProps<typeof inputVariants> {
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
error?: string;
success?: string;
wrapperClassName?: string;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
(
{
className,
type,
variant,
size,
leftIcon,
rightIcon,
error,
success,
wrapperClassName,
...props
},
ref,
) => {
const inputVariant = error ? 'error' : success ? 'success' : variant;
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
<div className={cn('relative w-full', wrapperClassName)}>
{leftIcon && (
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-dimmed pointer-events-none">
{leftIcon}
</div>
)}
ref={ref}
{...props}
/>
<input
type={type}
className={cn(
inputVariants({ variant: inputVariant, size }),
leftIcon && 'pl-10',
rightIcon && 'pr-10',
className,
)}
ref={ref}
{...props}
/>
{rightIcon && (
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-dimmed">
{rightIcon}
</div>
)}
{error && (
<p className="mt-1.5 text-xs text-destructive animate-fade-in">{error}</p>
)}
{success && !error && (
<p className="mt-1.5 text-xs text-success animate-fade-in">{success}</p>
)}
</div>
);
},
);
Input.displayName = 'Input';
export { Input };
export { Input, inputVariants };

View File

@@ -6,15 +6,37 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
'text-sm font-medium leading-none text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
{
variants: {
variant: {
default: '',
muted: 'text-muted-foreground font-normal',
dimmed: 'text-dimmed font-normal',
},
size: {
sm: 'text-xs',
default: 'text-sm',
lg: 'text-base',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
>(({ className, variant, size, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants({ variant, size }), className)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };
export { Label, labelVariants };

View File

@@ -0,0 +1,178 @@
'use client';
import * as React from 'react';
import { useTheme } from 'next-themes';
import { Sun, Moon, Monitor } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
interface ThemeToggleProps {
variant?: 'default' | 'compact' | 'dropdown';
className?: string;
}
export function ThemeToggle({ variant = 'default', className }: ThemeToggleProps) {
const { theme, setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return (
<div className={cn('h-9 w-9 rounded-lg bg-muted animate-pulse', className)} />
);
}
if (variant === 'compact') {
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
className={cn(
'relative h-9 w-9 rounded-lg transition-colors',
'hover:bg-primary/10 hover:text-primary',
className
)}
aria-label={`Switch to ${resolvedTheme === 'dark' ? 'light' : 'dark'} mode`}
>
<Sun className={cn(
'h-[1.125rem] w-[1.125rem] transition-all duration-300',
resolvedTheme === 'dark' ? 'rotate-0 scale-100' : 'rotate-90 scale-0'
)} />
<Moon className={cn(
'absolute h-[1.125rem] w-[1.125rem] transition-all duration-300',
resolvedTheme === 'dark' ? 'rotate-90 scale-0' : 'rotate-0 scale-100'
)} />
</Button>
);
}
// Default: segmented toggle with all three options
return (
<div
className={cn(
'inline-flex items-center rounded-lg p-1 gap-0.5',
'bg-muted/50 border border-border',
className
)}
role="radiogroup"
aria-label="Theme selection"
>
{[
{ value: 'light', icon: Sun, label: 'Aydinlik' },
{ value: 'dark', icon: Moon, label: 'Karanlik' },
{ value: 'system', icon: Monitor, label: 'Sistem' },
].map(({ value, icon: Icon, label }) => (
<button
key={value}
onClick={() => setTheme(value)}
className={cn(
'relative flex items-center justify-center rounded-md px-3 py-1.5 text-sm font-medium transition-all duration-200',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
theme === value
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground hover:bg-background/50'
)}
role="radio"
aria-checked={theme === value}
aria-label={label}
>
<Icon className="h-4 w-4 mr-1.5" />
<span className="hidden sm:inline">{label}</span>
</button>
))}
</div>
);
}
// Dropdown version for mobile
export function ThemeToggleDropdown({ className }: { className?: string }) {
const { theme, setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = React.useState(false);
const [isOpen, setIsOpen] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
React.useEffect(() => {
const handleClickOutside = () => setIsOpen(false);
if (isOpen) {
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}
}, [isOpen]);
if (!mounted) {
return <div className={cn('h-9 w-9 rounded-lg bg-muted animate-pulse', className)} />;
}
const themes = [
{ value: 'light', icon: Sun, label: 'Aydinlik Mod' },
{ value: 'dark', icon: Moon, label: 'Karanlik Mod' },
{ value: 'system', icon: Monitor, label: 'Sistem Ayari' },
];
const currentTheme = themes.find(t => t.value === theme) || themes[2];
const CurrentIcon = resolvedTheme === 'dark' ? Moon : Sun;
return (
<div className={cn('relative', className)}>
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
setIsOpen(!isOpen);
}}
className={cn(
'h-9 w-9 rounded-lg transition-colors',
'hover:bg-primary/10 hover:text-primary',
isOpen && 'bg-primary/10 text-primary'
)}
aria-label="Tema sec"
aria-expanded={isOpen}
>
<CurrentIcon className="h-[1.125rem] w-[1.125rem]" />
</Button>
{isOpen && (
<div
className={cn(
'absolute right-0 top-full mt-2 z-50',
'min-w-[160px] rounded-lg border border-border bg-popover p-1 shadow-lg',
'animate-scale-in origin-top-right'
)}
role="menu"
>
{themes.map(({ value, icon: Icon, label }) => (
<button
key={value}
onClick={() => {
setTheme(value);
setIsOpen(false);
}}
className={cn(
'flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
theme === value
? 'bg-primary/10 text-primary font-medium'
: 'text-foreground hover:bg-muted'
)}
role="menuitem"
>
<Icon className="h-4 w-4" />
{label}
{theme === value && (
<span className="ml-auto h-1.5 w-1.5 rounded-full bg-primary" />
)}
</button>
))}
</div>
)}
</div>
);
}

View File

@@ -24,12 +24,14 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-xl p-4 pr-8 ring ring-card-border shadow-card transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
{
variants: {
variant: {
default: 'border bg-background text-foreground',
destructive: 'destructive group border-destructive bg-destructive text-destructive-foreground',
default: 'bg-card text-foreground',
destructive: 'bg-destructive text-destructive-foreground ring-destructive/20',
success: 'bg-success text-success-foreground ring-success/20',
warning: 'bg-warning text-warning-foreground ring-warning/20',
},
},
defaultVariants: {
@@ -53,7 +55,7 @@ const ToastAction = React.forwardRef<
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
'inline-flex h-8 shrink-0 items-center justify-center rounded-lg ring ring-border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:ring-destructive/30 group-[.destructive]:hover:bg-destructive/90 group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className,
)}
{...props}
@@ -68,7 +70,7 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
'absolute right-2 top-2 rounded-lg p-1.5 text-foreground/50 opacity-0 transition-all hover:text-foreground hover:bg-muted focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring group-hover:opacity-100 group-[.destructive]:text-destructive-foreground/70 group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:hover:bg-destructive/80',
className,
)}
toast-close=""
@@ -91,7 +93,7 @@ const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description ref={ref} className={cn('text-sm opacity-90', className)} {...props} />
<ToastPrimitives.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;