first commit
This commit is contained in:
16
apps/frontend/components/ui/avatar.tsx
Normal file
16
apps/frontend/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Avatar({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return <AvatarPrimitive.Root className={cn('relative flex h-12 w-12 shrink-0 overflow-hidden rounded-full', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return <AvatarPrimitive.Image className={cn('aspect-square h-full w-full object-cover', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function AvatarFallback({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return <AvatarPrimitive.Fallback className={cn('flex h-full w-full items-center justify-center rounded-full bg-[#f4f5f8]', className)} {...props} />;
|
||||
}
|
||||
38
apps/frontend/components/ui/button.tsx
Normal file
38
apps/frontend/components/ui/button.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-2xl text-sm font-semibold transition disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-[#20212b] text-white hover:bg-[#11131d]',
|
||||
secondary: 'bg-[#f4f5f8] text-[#111827] hover:bg-[#eceef4]',
|
||||
outline: 'border border-[#d8dbe5] bg-transparent hover:bg-[#f4f5f8]',
|
||||
ghost: 'hover:bg-[#f4f5f8]',
|
||||
white: 'bg-white text-[#171821] hover:bg-[#f3f4f7]'
|
||||
},
|
||||
size: {
|
||||
default: 'h-11 px-5',
|
||||
sm: 'h-9 px-3',
|
||||
lg: 'h-14 px-8',
|
||||
icon: 'h-11 w-11'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} />;
|
||||
}
|
||||
22
apps/frontend/components/ui/card.tsx
Normal file
22
apps/frontend/components/ui/card.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn('rounded-[24px] border border-[#eceef4] bg-white text-[#111827] shadow-sm', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn('flex flex-col gap-1.5 p-6', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
|
||||
return <h3 className={cn('text-xl font-semibold tracking-tight', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
||||
return <p className={cn('text-sm text-[#667085]', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn('p-6 pt-0', className)} {...props} />;
|
||||
}
|
||||
34
apps/frontend/components/ui/command.tsx
Normal file
34
apps/frontend/components/ui/command.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { Command as CommandPrimitive } from 'cmdk';
|
||||
import { Search } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return <CommandPrimitive className={cn('flex h-full w-full flex-col overflow-hidden rounded-xl bg-white text-[#111827]', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CommandInput({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div className="flex h-11 items-center gap-2 border-b border-[#eceef4] px-3">
|
||||
<Search className="h-4 w-4 shrink-0 text-[#8a8f9e]" />
|
||||
<CommandPrimitive.Input className={cn('h-10 w-full bg-transparent text-sm outline-none placeholder:text-[#8a8f9e]', className)} {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return <CommandPrimitive.List className={cn('max-h-64 overflow-y-auto overflow-x-hidden p-1', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CommandEmpty({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return <CommandPrimitive.Empty className={cn('py-6 text-center text-sm text-[#667085]', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CommandGroup({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return <CommandPrimitive.Group className={cn('overflow-hidden p-1 text-sm', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return <CommandPrimitive.Item className={cn('flex cursor-pointer select-none items-center gap-3 rounded-xl px-3 py-2 text-sm outline-none data-[selected=true]:bg-[#f4f5f8]', className)} {...props} />;
|
||||
}
|
||||
30
apps/frontend/components/ui/dialog.tsx
Normal file
30
apps/frontend/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { X } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const Dialog = DialogPrimitive.Root;
|
||||
export const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
export function DialogContent({ className, children, ...props }: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||
return (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay className="fixed inset-0 z-[200] bg-black/50 backdrop-blur-[1px]" />
|
||||
<DialogPrimitive.Content className={cn('fixed left-1/2 top-1/2 z-[201] w-[min(92vw,520px)] -translate-x-1/2 -translate-y-1/2 rounded-[28px] bg-white p-6 shadow-2xl', className)} {...props}>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-5 top-5 rounded-full p-1 hover:bg-[#f4f5f8]" aria-label="Закрыть">
|
||||
<X className="h-4 w-4" />
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn('mb-5 flex flex-col gap-1', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return <DialogPrimitive.Title className={cn('text-xl font-semibold', className)} {...props} />;
|
||||
}
|
||||
15
apps/frontend/components/ui/dropdown-menu.tsx
Normal file
15
apps/frontend/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
export function DropdownMenuContent({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return <DropdownMenuPrimitive.Content className={cn('z-50 min-w-48 rounded-2xl border border-[#eceef4] bg-white p-2 shadow-xl', className)} sideOffset={8} {...props} />;
|
||||
}
|
||||
|
||||
export function DropdownMenuItem({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Item>) {
|
||||
return <DropdownMenuPrimitive.Item className={cn('flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2 text-sm outline-none hover:bg-[#f4f5f8]', className)} {...props} />;
|
||||
}
|
||||
20
apps/frontend/components/ui/form.tsx
Normal file
20
apps/frontend/components/ui/form.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Form({ className, ...props }: React.FormHTMLAttributes<HTMLFormElement>) {
|
||||
return <form className={cn('space-y-4', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function FormItem({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn('space-y-2', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return <LabelPrimitive.Root className={cn('text-sm font-medium', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function FormMessage({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
||||
return <p className={cn('text-sm text-red-600', className)} {...props} />;
|
||||
}
|
||||
15
apps/frontend/components/ui/input.tsx
Normal file
15
apps/frontend/components/ui/input.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Input({ className, type, ...props }: React.InputHTMLAttributes<HTMLInputElement>) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'h-12 w-full rounded-2xl border border-[#d8dbe5] bg-white px-4 text-sm outline-none transition placeholder:text-[#8a8f9e] focus:border-[#aeb3c2] focus:ring-4 focus:ring-[#eef0f6]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
91
apps/frontend/components/ui/otp-input.tsx
Normal file
91
apps/frontend/components/ui/otp-input.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface OtpInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onComplete?: (value: string) => void;
|
||||
length?: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function OtpInput({ value, onChange, onComplete, length = 6, disabled = false, className }: OtpInputProps) {
|
||||
const inputsRef = useRef<Array<HTMLInputElement | null>>([]);
|
||||
const digits = Array.from({ length }, (_, index) => value[index] ?? '');
|
||||
|
||||
function commit(next: string) {
|
||||
const clean = next.replace(/\D/g, '').slice(0, length);
|
||||
onChange(clean);
|
||||
if (clean.length === length) {
|
||||
onComplete?.(clean);
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(index: number, raw: string) {
|
||||
const char = raw.replace(/\D/g, '').slice(-1);
|
||||
if (!char) return;
|
||||
const chars = value.split('');
|
||||
chars[index] = char;
|
||||
const next = chars.join('').slice(0, length);
|
||||
commit(next);
|
||||
if (index < length - 1) {
|
||||
inputsRef.current[index + 1]?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(index: number, event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.key === 'Backspace') {
|
||||
event.preventDefault();
|
||||
const chars = value.split('');
|
||||
if (chars[index]) {
|
||||
chars[index] = '';
|
||||
onChange(chars.join(''));
|
||||
} else if (index > 0) {
|
||||
chars[index - 1] = '';
|
||||
onChange(chars.join(''));
|
||||
inputsRef.current[index - 1]?.focus();
|
||||
}
|
||||
}
|
||||
if (event.key === 'ArrowLeft' && index > 0) {
|
||||
inputsRef.current[index - 1]?.focus();
|
||||
}
|
||||
if (event.key === 'ArrowRight' && index < length - 1) {
|
||||
inputsRef.current[index + 1]?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function handlePaste(event: React.ClipboardEvent<HTMLInputElement>) {
|
||||
event.preventDefault();
|
||||
const pasted = event.clipboardData.getData('text').replace(/\D/g, '').slice(0, length);
|
||||
if (!pasted) return;
|
||||
commit(pasted);
|
||||
const focusIndex = Math.min(pasted.length, length - 1);
|
||||
inputsRef.current[focusIndex]?.focus();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex justify-between gap-2', className)}>
|
||||
{digits.map((digit, index) => (
|
||||
<input
|
||||
key={index}
|
||||
ref={(element) => {
|
||||
inputsRef.current[index] = element;
|
||||
}}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoComplete="one-time-code"
|
||||
maxLength={1}
|
||||
disabled={disabled}
|
||||
value={digit}
|
||||
onChange={(event) => handleChange(index, event.target.value)}
|
||||
onKeyDown={(event) => handleKeyDown(index, event)}
|
||||
onPaste={handlePaste}
|
||||
className="h-14 w-full rounded-2xl border border-[#555762] bg-transparent text-center text-2xl font-semibold text-white outline-none transition focus:border-[#7b7f8f] focus:ring-4 focus:ring-white/10 disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
140
apps/frontend/components/ui/phone-input.tsx
Normal file
140
apps/frontend/components/ui/phone-input.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import { ChevronDown, X } from 'lucide-react';
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface CountryOption {
|
||||
id: string;
|
||||
code: string;
|
||||
label: string;
|
||||
maxDigits: number;
|
||||
/** Path to the country flag asset inside the public directory. */
|
||||
flagSrc: string;
|
||||
}
|
||||
|
||||
export const FLAG_WIDTH = 24;
|
||||
export const FLAG_HEIGHT = 18;
|
||||
|
||||
export const phoneCountries: CountryOption[] = [
|
||||
{ id: 'RU', code: '+7', label: 'Россия', maxDigits: 10, flagSrc: '/flags/ru.svg' },
|
||||
{ id: 'BY', code: '+375', label: 'Беларусь', maxDigits: 9, flagSrc: '/flags/by.svg' },
|
||||
{ id: 'KZ', code: '+7', label: 'Казахстан', maxDigits: 10, flagSrc: '/flags/kz.svg' }
|
||||
];
|
||||
|
||||
function FlagIcon({ src, className }: { src: string; className?: string }) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn('inline-block shrink-0 overflow-hidden rounded-[3px] bg-center bg-no-repeat shadow-[inset_0_0_0_1px_rgba(0,0,0,0.08)]', className)}
|
||||
style={{
|
||||
width: `${FLAG_WIDTH}px`,
|
||||
height: `${FLAG_HEIGHT}px`,
|
||||
backgroundImage: `url('${src}')`,
|
||||
backgroundSize: 'cover'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function formatPhoneNumber(digits: string, country: CountryOption) {
|
||||
const clean = digits.replace(/\D/g, '').slice(0, country.maxDigits);
|
||||
if (country.code === '+375') {
|
||||
const part1 = clean.slice(0, 2);
|
||||
const part2 = clean.slice(2, 5);
|
||||
const part3 = clean.slice(5, 7);
|
||||
const part4 = clean.slice(7, 9);
|
||||
if (clean.length <= 2) return part1 ? `(${part1}` : '';
|
||||
if (clean.length <= 5) return `(${part1}) ${part2}`;
|
||||
if (clean.length <= 7) return `(${part1}) ${part2}-${part3}`;
|
||||
return `(${part1}) ${part2}-${part3}-${part4}`;
|
||||
}
|
||||
|
||||
const part1 = clean.slice(0, 3);
|
||||
const part2 = clean.slice(3, 6);
|
||||
const part3 = clean.slice(6, 8);
|
||||
const part4 = clean.slice(8, 10);
|
||||
if (clean.length <= 3) return part1 ? `(${part1}` : '';
|
||||
if (clean.length <= 6) return `(${part1}) ${part2}`;
|
||||
if (clean.length <= 8) return `(${part1}) ${part2}-${part3}`;
|
||||
return `(${part1}) ${part2}-${part3}-${part4}`;
|
||||
}
|
||||
|
||||
export function toE164(country: CountryOption, digits: string) {
|
||||
return `${country.code}${digits.replace(/\D/g, '').slice(0, country.maxDigits)}`;
|
||||
}
|
||||
|
||||
export function PhoneInput({
|
||||
country,
|
||||
value,
|
||||
onCountryChange,
|
||||
onValueChange,
|
||||
className
|
||||
}: {
|
||||
country: CountryOption;
|
||||
value: string;
|
||||
onCountryChange: (country: CountryOption) => void;
|
||||
onValueChange: (digits: string) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
const maskedValue = formatPhoneNumber(value, country);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-[58px] w-full items-center rounded-2xl border border-[#555762] bg-transparent text-white transition focus-within:border-[#7b7f8f] focus-within:ring-4 focus-within:ring-white/10',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button type="button" className="flex h-full shrink-0 items-center gap-2 rounded-l-2xl px-4 text-white outline-none hover:bg-[#2a2c36]" aria-label="Выбрать страну">
|
||||
<FlagIcon src={country.flagSrc} />
|
||||
<span className="text-base font-semibold">{country.code}</span>
|
||||
<ChevronDown className="h-4 w-4 text-[#8f92a0]" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-1">
|
||||
<Command>
|
||||
<CommandInput placeholder="Страна или код" />
|
||||
<CommandList>
|
||||
<CommandEmpty>Страна не найдена</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{phoneCountries.map((item) => (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
value={`${item.label} ${item.code}`}
|
||||
onSelect={() => {
|
||||
onCountryChange(item);
|
||||
onValueChange(value.replace(/\D/g, '').slice(0, item.maxDigits));
|
||||
}}
|
||||
>
|
||||
<FlagIcon src={item.flagSrc} />
|
||||
<span className="flex-1">{item.label}</span>
|
||||
<span className="font-semibold text-[#667085]">{item.code}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<input
|
||||
className="h-full min-w-0 flex-1 bg-transparent px-1 text-lg text-white outline-none placeholder:text-[#8f92a0]"
|
||||
inputMode="tel"
|
||||
placeholder={country.code === '+375' ? '(29) 123-45-67' : '(999) 123-45-67'}
|
||||
value={maskedValue}
|
||||
onChange={(event) => onValueChange(event.target.value.replace(/\D/g, '').slice(0, country.maxDigits))}
|
||||
required
|
||||
/>
|
||||
|
||||
{value ? (
|
||||
<button type="button" className="mr-3 flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-[#8f92a0] hover:bg-[#2a2c36] hover:text-white" onClick={() => onValueChange('')} aria-label="Очистить телефон">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
apps/frontend/components/ui/popover.tsx
Normal file
20
apps/frontend/components/ui/popover.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const Popover = PopoverPrimitive.Root;
|
||||
export const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
export function PopoverContent({ className, align = 'start', sideOffset = 8, ...props }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn('z-50 w-72 rounded-2xl border border-[#eceef4] bg-white p-2 text-[#111827] shadow-xl outline-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
26
apps/frontend/components/ui/table.tsx
Normal file
26
apps/frontend/components/ui/table.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Table({ className, ...props }: React.TableHTMLAttributes<HTMLTableElement>) {
|
||||
return <table className={cn('w-full caption-bottom text-sm', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function TableHeader({ className, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) {
|
||||
return <thead className={cn('[&_tr]:border-b', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function TableBody({ className, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) {
|
||||
return <tbody className={cn('[&_tr:last-child]:border-0', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function TableRow({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) {
|
||||
return <tr className={cn('border-b border-[#eceef4] transition-colors hover:bg-[#f8f9fb]', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function TableHead({ className, ...props }: React.ThHTMLAttributes<HTMLTableCellElement>) {
|
||||
return <th className={cn('h-11 px-4 text-left align-middle font-medium text-[#667085]', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function TableCell({ className, ...props }: React.TdHTMLAttributes<HTMLTableCellElement>) {
|
||||
return <td className={cn('p-4 align-middle', className)} {...props} />;
|
||||
}
|
||||
23
apps/frontend/components/ui/tabs.tsx
Normal file
23
apps/frontend/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const Tabs = TabsPrimitive.Root;
|
||||
|
||||
export function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return <TabsPrimitive.List className={cn('inline-flex rounded-2xl bg-[#090a0f] p-0.5', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
className={cn('rounded-[15px] px-9 py-3 text-sm font-semibold text-[#9a9dab] data-[state=active]:bg-[#22232c] data-[state=active]:text-white', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return <TabsPrimitive.Content className={cn('mt-4', className)} {...props} />;
|
||||
}
|
||||
17
apps/frontend/components/ui/toast.tsx
Normal file
17
apps/frontend/components/ui/toast.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import * as ToastPrimitive from '@radix-ui/react-toast';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const ToastProvider = ToastPrimitive.Provider;
|
||||
export const ToastViewport = (props: React.ComponentProps<typeof ToastPrimitive.Viewport>) => (
|
||||
<ToastPrimitive.Viewport className="fixed bottom-4 right-4 z-50 flex max-w-sm flex-col gap-2" {...props} />
|
||||
);
|
||||
|
||||
export function Toast({ className, ...props }: React.ComponentProps<typeof ToastPrimitive.Root>) {
|
||||
return <ToastPrimitive.Root className={cn('rounded-2xl bg-[#20212b] p-4 text-sm text-white shadow-xl', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function ToastTitle({ className, ...props }: React.ComponentProps<typeof ToastPrimitive.Title>) {
|
||||
return <ToastPrimitive.Title className={cn('font-semibold', className)} {...props} />;
|
||||
}
|
||||
Reference in New Issue
Block a user