From d5e44223d8c6749557b237aeda3bfa3d510e04a6 Mon Sep 17 00:00:00 2001 From: Rami Bitar Date: Mon, 31 Aug 2026 11:41:51 -0400 Subject: [PATCH] Add complete shadcn component set --- app/globals.css | 16 ++ components/ui/accordion.tsx | 212 ++++++++++++++++++--- components/ui/alert.tsx | 60 ++++++ components/ui/avatar.tsx | 81 ++++++++ components/ui/badge.tsx | 48 +++++ components/ui/breadcrumb.tsx | 149 +++++++++++++++ components/ui/button-group.tsx | 83 +++++++++ components/ui/button.tsx | 71 +++++-- components/ui/card.tsx | 94 ++++++++-- components/ui/carousel.tsx | 265 +++++++++++++++++++++----- components/ui/collapsible.tsx | 33 ++++ components/ui/command.tsx | 192 +++++++++++++++++++ components/ui/dialog.tsx | 158 ++++++++++++++++ components/ui/dropdown-menu.tsx | 257 ++++++++++++++++++++++++++ components/ui/empty.tsx | 105 +++++++++++ components/ui/hover-card.tsx | 44 +++++ components/ui/input-group.tsx | 167 +++++++++++++++++ components/ui/input-otp.tsx | 86 +++++++++ components/ui/input.tsx | 19 +- components/ui/item.tsx | 210 +++++++++++++++++++++ components/ui/loader.tsx | 96 ++++++++++ components/ui/pagination.tsx | 128 +++++++++++++ components/ui/progress.tsx | 43 +++++ components/ui/rainbow-button.tsx | 61 ++++++ components/ui/remix-icon.tsx | 42 +++++ components/ui/scroll-area.tsx | 58 ++++++ components/ui/select.tsx | 190 +++++++++++++++++++ components/ui/separator.tsx | 28 +++ components/ui/sheet.tsx | 308 +++++++++++++++++++++++++++++++ components/ui/skeleton.tsx | 13 ++ components/ui/sonner.tsx | 23 +++ components/ui/spinner.tsx | 16 ++ components/ui/switch.tsx | 74 ++++++++ components/ui/table.tsx | 113 ++++++++++++ components/ui/tabs.tsx | 143 ++++++++++++++ components/ui/textarea.tsx | 17 +- components/ui/tooltip.tsx | 57 ++++++ package.json | 5 + yarn.lock | 99 +++++++++- 39 files changed, 3739 insertions(+), 125 deletions(-) create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/button-group.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/empty.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input-group.tsx create mode 100644 components/ui/input-otp.tsx create mode 100644 components/ui/item.tsx create mode 100644 components/ui/loader.tsx create mode 100644 components/ui/pagination.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/rainbow-button.tsx create mode 100644 components/ui/remix-icon.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/spinner.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/tooltip.tsx diff --git a/app/globals.css b/app/globals.css index d2e3e79..a172077 100644 --- a/app/globals.css +++ b/app/globals.css @@ -24,6 +24,11 @@ --ring: oklch(0.56 0.2 255); --brand: oklch(0.56 0.21 255); --brand-soft: oklch(0.94 0.03 250); + --color-1: oklch(66.2% 0.225 25.9); + --color-2: oklch(60.4% 0.26 302); + --color-3: oklch(69.6% 0.165 251); + --color-4: oklch(80.2% 0.134 225); + --color-5: oklch(90.7% 0.231 133); --radius: 0.625rem; } @@ -71,12 +76,23 @@ --color-ring: var(--ring); --color-brand: var(--brand); --color-brand-soft: var(--brand-soft); + --color-color-1: var(--color-1); + --color-color-2: var(--color-2); + --color-color-3: var(--color-3); + --color-color-4: var(--color-4); + --color-color-5: var(--color-5); + --animate-rainbow: rainbow var(--speed, 2s) infinite linear; --radius-sm: calc(var(--radius) - 6px); --radius-md: calc(var(--radius) - 3px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 6px); --font-sans: var(--font-outfit), ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --font-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + + @keyframes rainbow { + 0% { background-position: 0%; } + 100% { background-position: 200%; } + } } @layer base { diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx index 67ea349..5863962 100644 --- a/components/ui/accordion.tsx +++ b/components/ui/accordion.tsx @@ -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) { - return ; +interface AccordionContextType { + value: string | string[]; + onValueChange: (value: string) => void; + type: 'single' | 'multiple'; } -function AccordionItem({ className, ...props }: React.ComponentProps) { - return ; +const AccordionContext = createContext( + undefined +); + +interface AccordionItemContextType { + value: string; } -function AccordionTrigger({ className, children, ...props }: React.ComponentProps) { +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( + 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 ( - - svg]:rotate-180', className)} - {...props} - > - {children} - - + +
{children}
+
); } -function AccordionContent({ className, children, ...props }: React.ComponentProps) { +interface AccordionItemProps { + value: string; + children: React.ReactNode; + className?: string; +} + +function AccordionItem({ value, children, className }: AccordionItemProps) { return ( - -
{children}
-
+ +
+ {children} +
+
+ ); +} + +interface AccordionTriggerProps extends React.ButtonHTMLAttributes { + 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 ( +
+ +
+ ); +} + +interface AccordionContentProps extends React.HTMLAttributes { + 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 ( +
+
{children}
+
); } diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..3c5a3e4 --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,60 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +type AlertVariant = "default" | "destructive" + +const baseClasses = + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current" + +const variantClasses: Record = { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", +} + +function Alert({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & { variant?: AlertVariant }) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..6cd2e95 --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { cn } from '@/lib/utils'; + +interface AvatarProps extends React.ComponentProps<'div'> { + size?: 'sm' | 'md' | 'lg' | 'xl'; +} + +const sizeClasses = { + sm: 'size-6', + md: 'size-8', + lg: 'size-10', + xl: 'size-12', +}; + +function Avatar({ className, size = 'md', ...props }: AvatarProps) { + const [imageError, setImageError] = React.useState(false); + + return ( +
+ ); +} + +function AvatarImage({ + className, + onError, + ...props +}: React.ComponentProps<'img'>) { + const [hasError, setHasError] = React.useState(false); + + const handleError = (e: React.SyntheticEvent) => { + setHasError(true); + onError?.(e as any); + }; + + if (hasError) { + return null; + } + + return ( + + ); +} + +interface AvatarFallbackProps extends React.ComponentProps<'div'> { + children: React.ReactNode; +} + +function AvatarFallback({ + className, + children, + ...props +}: AvatarFallbackProps) { + return ( +
+ {children} +
+ ); +} + +export { Avatar, AvatarImage, AvatarFallback }; +export type { AvatarProps }; diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..6eb2a05 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", + outline: + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + link: "text-primary underline-offset-4 [a&]:hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..9313497 --- /dev/null +++ b/components/ui/breadcrumb.tsx @@ -0,0 +1,149 @@ +import React from 'react'; +import { cn } from '@/lib/utils'; + +function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) { + return