Add complete shadcn component set

This commit is contained in:
Rami Bitar
2026-08-31 11:41:51 -04:00
parent 3e072967e6
commit d5e44223d8
39 changed files with 3739 additions and 125 deletions
+188 -24
View File
@@ -1,38 +1,202 @@
'use client';
import * as React from 'react';
import { ChevronDown } from 'lucide-react';
import { Accordion as AccordionPrimitive } from 'radix-ui';
import React, { createContext, useContext, useState, useCallback } from 'react';
import { cn } from '@/lib/utils';
function Accordion(props: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
interface AccordionContextType {
value: string | string[];
onValueChange: (value: string) => void;
type: 'single' | 'multiple';
}
function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return <AccordionPrimitive.Item data-slot="accordion-item" className={cn('border-b', className)} {...props} />;
const AccordionContext = createContext<AccordionContextType | undefined>(
undefined
);
interface AccordionItemContextType {
value: string;
}
function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
const AccordionItemContext = createContext<
AccordionItemContextType | undefined
>(undefined);
function useAccordion() {
const context = useContext(AccordionContext);
if (!context) {
throw new Error('Accordion components must be used within an Accordion');
}
return context;
}
function useAccordionItem() {
const context = useContext(AccordionItemContext);
if (!context) {
throw new Error(
'AccordionTrigger and AccordionContent must be used within an AccordionItem'
);
}
return context;
}
interface AccordionProps {
type?: 'single' | 'multiple';
value?: string | string[];
defaultValue?: string | string[];
onValueChange?: (value: string | string[]) => void;
collapsible?: boolean;
className?: string;
children: React.ReactNode;
}
function Accordion({
type = 'single',
value: controlledValue,
defaultValue,
onValueChange,
collapsible = true,
className,
children,
}: AccordionProps) {
const [internalValue, setInternalValue] = useState<string | string[]>(
defaultValue ?? (type === 'single' ? '' : [])
);
const isControlled = controlledValue !== undefined;
const value = isControlled ? controlledValue : internalValue;
const handleValueChange = useCallback(
(itemValue: string) => {
if (type === 'single') {
const newValue = value === itemValue && collapsible ? '' : itemValue;
if (!isControlled) {
setInternalValue(newValue);
}
onValueChange?.(newValue);
} else {
const valueArray = Array.isArray(value) ? value : [];
const newValue = valueArray.includes(itemValue)
? valueArray.filter((v) => v !== itemValue)
: [...valueArray, itemValue];
if (!isControlled) {
setInternalValue(newValue);
}
onValueChange?.(newValue);
}
},
[value, type, isControlled, onValueChange, collapsible]
);
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn('flex flex-1 items-start justify-between gap-6 py-6 text-left text-lg font-medium outline-none transition-colors hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring/50 [&[data-state=open]>svg]:rotate-180', className)}
{...props}
>
{children}
<ChevronDown className="mt-1 size-5 shrink-0 transition-transform duration-200" aria-hidden="true" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
<AccordionContext.Provider
value={{ value, onValueChange: handleValueChange, type }}
>
<div data-slot="accordion" className={className}>{children}</div>
</AccordionContext.Provider>
);
}
function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
interface AccordionItemProps {
value: string;
children: React.ReactNode;
className?: string;
}
function AccordionItem({ value, children, className }: AccordionItemProps) {
return (
<AccordionPrimitive.Content data-slot="accordion-content" className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" {...props}>
<div className={cn('pb-6', className)}>{children}</div>
</AccordionPrimitive.Content>
<AccordionItemContext.Provider value={{ value }}>
<div
data-slot="accordion-item"
className={cn('border-b border-border last:border-b-0', className)}
data-value={value}
>
{children}
</div>
</AccordionItemContext.Provider>
);
}
interface AccordionTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
}
function AccordionTrigger({
className,
children,
...props
}: AccordionTriggerProps) {
const accordion = useAccordion();
const item = useAccordionItem();
const handleClick = () => {
accordion.onValueChange(item.value);
};
const isOpen =
accordion.type === 'single'
? accordion.value === item.value
: Array.isArray(accordion.value) && accordion.value.includes(item.value);
return (
<div className="flex">
<button
data-slot="accordion-trigger"
className={cn(
'flex flex-1 items-start justify-between gap-4 rounded-md py-4 px-0 text-left text-sm font-medium transition-all outline-none hover:cursor-pointer focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:rounded-md disabled:pointer-events-none disabled:opacity-50',
isOpen && '[&>svg]:rotate-180',
className
)}
onClick={handleClick}
data-state={isOpen ? 'open' : 'closed'}
{...props}
>
{children}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
);
}
interface AccordionContentProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
function AccordionContent({
className,
children,
...props
}: AccordionContentProps) {
const accordion = useAccordion();
const item = useAccordionItem();
const isOpen =
accordion.type === 'single'
? accordion.value === item.value
: Array.isArray(accordion.value) && accordion.value.includes(item.value);
return (
<div
data-slot="accordion-content"
data-state={isOpen ? 'open' : 'closed'}
className={cn(
'overflow-hidden text-sm transition-all duration-200',
isOpen ? 'max-h-96' : 'max-h-0'
)}
{...props}
>
<div className={cn('pt-0 pb-4', className)}>{children}</div>
</div>
);
}