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