first commit
This commit is contained in:
202
components/ui/accordion.tsx
Normal file
202
components/ui/accordion.tsx
Normal file
@ -0,0 +1,202 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AccordionContextValue {
|
||||
value: string[];
|
||||
onValueChange: (value: string[]) => void;
|
||||
}
|
||||
|
||||
const AccordionContext = React.createContext<AccordionContextValue | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
interface AccordionProps {
|
||||
type?: "single" | "multiple";
|
||||
defaultValue?: string | string[];
|
||||
value?: string | string[];
|
||||
onValueChange?: (value: string | string[]) => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
|
||||
({ type = "single", defaultValue, value, onValueChange, children, className }, ref) => {
|
||||
const [internalValue, setInternalValue] = React.useState<string[]>(
|
||||
defaultValue
|
||||
? Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [defaultValue]
|
||||
: []
|
||||
);
|
||||
|
||||
const controlledValue = value
|
||||
? Array.isArray(value)
|
||||
? value
|
||||
: [value]
|
||||
: undefined;
|
||||
|
||||
const currentValue = controlledValue ?? internalValue;
|
||||
|
||||
const handleValueChange = React.useCallback(
|
||||
(newValue: string[]) => {
|
||||
if (!controlledValue) {
|
||||
setInternalValue(newValue);
|
||||
}
|
||||
if (onValueChange) {
|
||||
onValueChange(type === "single" ? newValue[0] || "" : newValue);
|
||||
}
|
||||
},
|
||||
[controlledValue, onValueChange, type]
|
||||
);
|
||||
|
||||
const contextValue = React.useMemo(
|
||||
() => ({
|
||||
value: currentValue,
|
||||
onValueChange: handleValueChange,
|
||||
}),
|
||||
[currentValue, handleValueChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={contextValue}>
|
||||
<div ref={ref} className={cn("space-y-2", className)}>
|
||||
{children}
|
||||
</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
Accordion.displayName = "Accordion";
|
||||
|
||||
interface AccordionItemProps {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const AccordionItem = React.forwardRef<HTMLDivElement, AccordionItemProps>(
|
||||
({ value, children, className }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-xl border border-gray-200 overflow-hidden", className)}
|
||||
data-value={value}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
interface AccordionTriggerProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
AccordionTriggerProps
|
||||
>(({ children, className }, ref) => {
|
||||
const context = React.useContext(AccordionContext);
|
||||
if (!context) {
|
||||
throw new Error("AccordionTrigger must be used within Accordion");
|
||||
}
|
||||
|
||||
const item = React.useContext(ItemContext);
|
||||
if (!item) {
|
||||
throw new Error("AccordionTrigger must be used within AccordionItem");
|
||||
}
|
||||
|
||||
const isOpen = context.value.includes(item.value);
|
||||
|
||||
const handleClick = () => {
|
||||
const newValue = isOpen
|
||||
? context.value.filter((v) => v !== item.value)
|
||||
: [...context.value, item.value];
|
||||
context.onValueChange(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between p-4 text-left font-medium text-primary transition-all hover:bg-secondary [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
data-state={isOpen ? "open" : "closed"}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-5 w-5 shrink-0 transition-transform duration-200" />
|
||||
</button>
|
||||
);
|
||||
});
|
||||
AccordionTrigger.displayName = "AccordionTrigger";
|
||||
|
||||
interface AccordionContentProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ItemContext = React.createContext<{ value: string } | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
AccordionContentProps
|
||||
>(({ children, className }, ref) => {
|
||||
const context = React.useContext(AccordionContext);
|
||||
if (!context) {
|
||||
throw new Error("AccordionContent must be used within Accordion");
|
||||
}
|
||||
|
||||
const item = React.useContext(ItemContext);
|
||||
if (!item) {
|
||||
throw new Error("AccordionContent must be used within AccordionItem");
|
||||
}
|
||||
|
||||
const isOpen = context.value.includes(item.value);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden transition-all",
|
||||
isOpen ? "max-h-[1000px] opacity-100" : "max-h-0 opacity-0"
|
||||
)}
|
||||
>
|
||||
<div className={cn("p-4 pt-0 text-gray-700", className)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
AccordionContent.displayName = "AccordionContent";
|
||||
|
||||
const AccordionItemWithContext = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
AccordionItemProps
|
||||
>(({ value, children, ...props }, ref) => {
|
||||
return (
|
||||
<ItemContext.Provider value={{ value }}>
|
||||
<AccordionItem ref={ref} value={value} {...props}>
|
||||
{children}
|
||||
</AccordionItem>
|
||||
</ItemContext.Provider>
|
||||
);
|
||||
});
|
||||
AccordionItemWithContext.displayName = "AccordionItem";
|
||||
|
||||
export {
|
||||
Accordion,
|
||||
AccordionItemWithContext as AccordionItem,
|
||||
AccordionTrigger,
|
||||
AccordionContent,
|
||||
};
|
||||
|
||||
46
components/ui/button.tsx
Normal file
46
components/ui/button.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-xl text-base font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
outline: "border-2 border-primary text-primary hover:bg-primary hover:text-white",
|
||||
ghost: "hover:bg-secondary hover:text-secondary-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-12 px-6 py-3",
|
||||
sm: "h-10 px-4",
|
||||
lg: "h-14 px-8 text-lg",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
|
||||
76
components/ui/card.tsx
Normal file
76
components/ui/card.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-2xl border border-gray-200 bg-white shadow-sm",
|
||||
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}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("text-xl font-semibold leading-none tracking-tight text-primary", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-gray-600", 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} />
|
||||
));
|
||||
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}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
||||
|
||||
Reference in New Issue
Block a user