Enhance frontend-web with new features and dependencies
- Added new font support for 'Oxanium' and integrated Radix UI components including Dialog and Select. - Updated CSS to set the default font to 'Oxanium Variable' and adjusted HTML font size. - Introduced AddAccountDialog component for managing debt accounts, enhancing user experience. - Refactored DebtsPage to utilize the new AddAccountDialog and improved account management features. - Updated Redux store to support debt categories and accounts, including actions for adding, updating, and removing accounts. - Mock data added for clients and invoices to facilitate development and testing.
This commit is contained in:
239
frontend-web/src/components/AddAccountDialog.tsx
Normal file
239
frontend-web/src/components/AddAccountDialog.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import {useState} from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Input} from '@/components/ui/input';
|
||||
import {Label} from '@/components/ui/label';
|
||||
import {useAppDispatch, useAppSelector, addAccount} from '@/store';
|
||||
|
||||
interface AddAccountDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export default function AddAccountDialog({open, onOpenChange}: AddAccountDialogProps) {
|
||||
const dispatch = useAppDispatch();
|
||||
const {categories} = useAppSelector(state => state.debts);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
categoryId: '',
|
||||
institution: '',
|
||||
accountNumber: '',
|
||||
originalBalance: '',
|
||||
currentBalance: '',
|
||||
interestRate: '',
|
||||
minimumPayment: '',
|
||||
dueDay: '',
|
||||
notes: '',
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const account = {
|
||||
id: crypto.randomUUID(),
|
||||
name: formData.name,
|
||||
categoryId: formData.categoryId,
|
||||
institution: formData.institution,
|
||||
accountNumber: formData.accountNumber || undefined,
|
||||
originalBalance: parseFloat(formData.originalBalance) || 0,
|
||||
currentBalance: parseFloat(formData.currentBalance) || parseFloat(formData.originalBalance) || 0,
|
||||
interestRate: parseFloat(formData.interestRate) || 0,
|
||||
minimumPayment: parseFloat(formData.minimumPayment) || 0,
|
||||
dueDay: parseInt(formData.dueDay) || 1,
|
||||
notes: formData.notes || undefined,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
dispatch(addAccount(account));
|
||||
onOpenChange(false);
|
||||
setFormData({
|
||||
name: '',
|
||||
categoryId: '',
|
||||
institution: '',
|
||||
accountNumber: '',
|
||||
originalBalance: '',
|
||||
currentBalance: '',
|
||||
interestRate: '',
|
||||
minimumPayment: '',
|
||||
dueDay: '',
|
||||
notes: '',
|
||||
});
|
||||
};
|
||||
|
||||
const updateField = (field: string, value: string) => {
|
||||
setFormData(prev => ({...prev, [field]: value}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="card-elevated max-h-[90vh] overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Debt Account</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new debt account to track your payoff progress
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Account Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="e.g., Chase Sapphire"
|
||||
value={formData.name}
|
||||
onChange={e => updateField('name', e.target.value)}
|
||||
className="input-depth"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="category">Category</Label>
|
||||
<Select
|
||||
value={formData.categoryId}
|
||||
onValueChange={value => updateField('categoryId', value)}
|
||||
required
|
||||
>
|
||||
<SelectTrigger className="input-depth">
|
||||
<SelectValue placeholder="Select a category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map(category => (
|
||||
<SelectItem key={category.id} value={category.id}>
|
||||
{category.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="institution">Institution / Lender</Label>
|
||||
<Input
|
||||
id="institution"
|
||||
placeholder="e.g., Chase Bank"
|
||||
value={formData.institution}
|
||||
onChange={e => updateField('institution', e.target.value)}
|
||||
className="input-depth"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="accountNumber">Last 4 Digits (optional)</Label>
|
||||
<Input
|
||||
id="accountNumber"
|
||||
placeholder="1234"
|
||||
maxLength={4}
|
||||
value={formData.accountNumber}
|
||||
onChange={e => updateField('accountNumber', e.target.value.replace(/\D/g, ''))}
|
||||
className="input-depth"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="originalBalance">Original Balance</Label>
|
||||
<Input
|
||||
id="originalBalance"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
value={formData.originalBalance}
|
||||
onChange={e => updateField('originalBalance', e.target.value)}
|
||||
className="input-depth"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="currentBalance">Current Balance</Label>
|
||||
<Input
|
||||
id="currentBalance"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
value={formData.currentBalance}
|
||||
onChange={e => updateField('currentBalance', e.target.value)}
|
||||
className="input-depth"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="interestRate">Interest Rate (%)</Label>
|
||||
<Input
|
||||
id="interestRate"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="100"
|
||||
placeholder="0.00"
|
||||
value={formData.interestRate}
|
||||
onChange={e => updateField('interestRate', e.target.value)}
|
||||
className="input-depth"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="minimumPayment">Min Payment</Label>
|
||||
<Input
|
||||
id="minimumPayment"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
value={formData.minimumPayment}
|
||||
onChange={e => updateField('minimumPayment', e.target.value)}
|
||||
className="input-depth"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="dueDay">Due Day of Month</Label>
|
||||
<Input
|
||||
id="dueDay"
|
||||
type="number"
|
||||
min="1"
|
||||
max="31"
|
||||
placeholder="1"
|
||||
value={formData.dueDay}
|
||||
onChange={e => updateField('dueDay', e.target.value)}
|
||||
className="input-depth"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={!formData.name || !formData.categoryId || !formData.institution}>
|
||||
Add Account
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,53 +1,50 @@
|
||||
import {NavLink, Outlet} from 'react-router-dom';
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from '@/components/ui/tooltip';
|
||||
import {TrendingUp, CreditCard, FileText, Users} from 'lucide-react';
|
||||
|
||||
const navItems = [
|
||||
{to: '/', label: 'Net Worth', icon: '◈'},
|
||||
{to: '/debts', label: 'Debts', icon: '◇'},
|
||||
{to: '/invoices', label: 'Invoices', icon: '▤'},
|
||||
{to: '/clients', label: 'Clients', icon: '◉'},
|
||||
{to: '/', label: 'Net Worth', icon: TrendingUp},
|
||||
{to: '/debts', label: 'Debts', icon: CreditCard},
|
||||
{to: '/invoices', label: 'Invoices', icon: FileText},
|
||||
{to: '/clients', label: 'Clients', icon: Users},
|
||||
];
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div className="flex min-h-screen">
|
||||
{/* Sidebar */}
|
||||
<aside className="fixed left-0 top-0 z-40 flex h-screen w-16 flex-col border-r border-border bg-sidebar">
|
||||
<div className="flex h-16 items-center justify-center border-b border-border">
|
||||
<span className="text-xl font-semibold text-foreground">W</span>
|
||||
</div>
|
||||
<nav className="flex flex-1 flex-col items-center gap-2 py-4">
|
||||
{navItems.map(item => (
|
||||
<Tooltip key={item.to}>
|
||||
<TooltipTrigger asChild>
|
||||
<NavLink
|
||||
to={item.to}
|
||||
className={({isActive}) =>
|
||||
`flex h-10 w-10 items-center justify-center rounded-lg text-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-accent text-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
|
||||
}`
|
||||
}
|
||||
>
|
||||
{item.icon}
|
||||
</NavLink>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
{item.label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
<div className="flex min-h-screen">
|
||||
{/* Sidebar */}
|
||||
<aside className="group fixed left-0 top-0 z-40 flex h-screen w-14 hover:w-44 flex-col border-r border-border bg-sidebar transition-all duration-200 ease-out">
|
||||
<div className="flex h-12 items-center gap-2 border-b border-border px-4">
|
||||
<span className="text-base font-semibold text-foreground">W</span>
|
||||
<span className="text-sm font-medium text-foreground opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
|
||||
Wealth
|
||||
</span>
|
||||
</div>
|
||||
<nav className="flex flex-1 flex-col gap-1 py-3 px-2">
|
||||
{navItems.map(item => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({isActive}) =>
|
||||
`flex h-9 items-center gap-3 rounded-lg px-2.5 transition-colors ${
|
||||
isActive
|
||||
? 'bg-accent text-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<item.icon className="h-[18px] w-[18px] shrink-0" />
|
||||
<span className="text-sm opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
|
||||
{item.label}
|
||||
</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="ml-16 flex-1">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
{/* Main content */}
|
||||
<main className="ml-14 flex-1">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
141
frontend-web/src/components/ui/dialog.tsx
Normal file
141
frontend-web/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
187
frontend-web/src/components/ui/select.tsx
Normal file
187
frontend-web/src/components/ui/select.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
align = "center",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
align={align}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
Reference in New Issue
Block a user