diff options
| author | Maze <167211895+mazeincoding@users.noreply.github.com> | 2026-01-31 00:20:04 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-31 00:20:04 +0100 |
| commit | 7bf0984698e43b9aa2dd879f65951318ed410390 (patch) | |
| tree | ccf2cd590a47d550152afc07129cb55c71a9ec35 /apps/web/src/components/ui | |
| parent | 0173db99448d1c4c6415dcf15272e17551b48b04 (diff) | |
| download | opencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz | |
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/components/ui')
60 files changed, 4154 insertions, 4891 deletions
diff --git a/apps/web/src/components/ui/accordion.tsx b/apps/web/src/components/ui/accordion.tsx index 68edd869..95a1c2df 100644 --- a/apps/web/src/components/ui/accordion.tsx +++ b/apps/web/src/components/ui/accordion.tsx @@ -4,53 +4,53 @@ import * as React from "react"; import { Accordion as AccordionPrimitive } from "radix-ui"; import { ChevronDown } from "lucide-react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Accordion = AccordionPrimitive.Root; const AccordionItem = React.forwardRef< - React.ElementRef<typeof AccordionPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> + React.ElementRef<typeof AccordionPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> >(({ className, ...props }, ref) => ( - <AccordionPrimitive.Item - ref={ref} - className={cn("border-b", className)} - {...props} - /> + <AccordionPrimitive.Item + ref={ref} + className={cn("border-b", className)} + {...props} + /> )); AccordionItem.displayName = "AccordionItem"; const AccordionTrigger = React.forwardRef< - React.ElementRef<typeof AccordionPrimitive.Trigger>, - React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> + React.ElementRef<typeof AccordionPrimitive.Trigger>, + React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> >(({ className, children, ...props }, ref) => ( - <AccordionPrimitive.Header className="flex"> - <AccordionPrimitive.Trigger - ref={ref} - className={cn( - "flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180", - className - )} - {...props} - > - {children} - <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" /> - </AccordionPrimitive.Trigger> - </AccordionPrimitive.Header> + <AccordionPrimitive.Header className="flex"> + <AccordionPrimitive.Trigger + ref={ref} + className={cn( + "flex flex-1 cursor-pointer items-center justify-between py-4 text-left text-sm font-medium hover:underline [&[data-state=open]>svg]:rotate-180", + className, + )} + {...props} + > + {children} + <ChevronDown className="text-muted-foreground size-4 shrink-0" /> + </AccordionPrimitive.Trigger> + </AccordionPrimitive.Header> )); AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; const AccordionContent = React.forwardRef< - React.ElementRef<typeof AccordionPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> + React.ElementRef<typeof AccordionPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> >(({ className, children, ...props }, ref) => ( - <AccordionPrimitive.Content - ref={ref} - className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" - {...props} - > - <div className={cn("pb-4 pt-0", className)}>{children}</div> - </AccordionPrimitive.Content> + <AccordionPrimitive.Content + ref={ref} + className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm" + {...props} + > + <div className={cn("pt-0 pb-4", className)}>{children}</div> + </AccordionPrimitive.Content> )); AccordionContent.displayName = AccordionPrimitive.Content.displayName; diff --git a/apps/web/src/components/ui/alert-dialog.tsx b/apps/web/src/components/ui/alert-dialog.tsx index 2cce235c..e7295429 100644 --- a/apps/web/src/components/ui/alert-dialog.tsx +++ b/apps/web/src/components/ui/alert-dialog.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import { AlertDialog as AlertDialogPrimitive } from "radix-ui"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; import { buttonVariants } from "./button"; const AlertDialog = AlertDialogPrimitive.Root; @@ -13,129 +13,129 @@ const AlertDialogTrigger = AlertDialogPrimitive.Trigger; const AlertDialogPortal = AlertDialogPrimitive.Portal; const AlertDialogOverlay = React.forwardRef< - React.ElementRef<typeof AlertDialogPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> + React.ElementRef<typeof AlertDialogPrimitive.Overlay>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> >(({ className, ...props }, ref) => ( - <AlertDialogPrimitive.Overlay - className={cn( - "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", - className - )} - {...props} - ref={ref} - /> + <AlertDialogPrimitive.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/80", + className, + )} + {...props} + ref={ref} + /> )); AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; const AlertDialogContent = React.forwardRef< - React.ElementRef<typeof AlertDialogPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> + React.ElementRef<typeof AlertDialogPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> >(({ className, ...props }, ref) => ( - <AlertDialogPortal> - <AlertDialogOverlay /> - <AlertDialogPrimitive.Content - ref={ref} - className={cn( - "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", - className - )} - {...props} - /> - </AlertDialogPortal> + <AlertDialogPortal> + <AlertDialogOverlay /> + <AlertDialogPrimitive.Content + ref={ref} + 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 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg", + className, + )} + {...props} + /> + </AlertDialogPortal> )); AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; const AlertDialogHeader = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col space-y-2 text-center sm:text-left", - className - )} - {...props} - /> + <div + className={cn( + "flex flex-col space-y-2 text-center sm:text-left", + className, + )} + {...props} + /> ); AlertDialogHeader.displayName = "AlertDialogHeader"; const AlertDialogFooter = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", - className - )} - {...props} - /> + <div + className={cn( + "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4", + className, + )} + {...props} + /> ); AlertDialogFooter.displayName = "AlertDialogFooter"; const AlertDialogTitle = React.forwardRef< - React.ElementRef<typeof AlertDialogPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> + React.ElementRef<typeof AlertDialogPrimitive.Title>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> >(({ className, ...props }, ref) => ( - <AlertDialogPrimitive.Title - ref={ref} - className={cn("text-lg font-semibold", className)} - {...props} - /> + <AlertDialogPrimitive.Title + ref={ref} + className={cn("text-lg font-semibold", className)} + {...props} + /> )); AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; const AlertDialogDescription = React.forwardRef< - React.ElementRef<typeof AlertDialogPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> + React.ElementRef<typeof AlertDialogPrimitive.Description>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> >(({ className, ...props }, ref) => ( - <AlertDialogPrimitive.Description - ref={ref} - className={cn("text-sm text-muted-foreground", className)} - {...props} - /> + <AlertDialogPrimitive.Description + ref={ref} + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> )); AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName; + AlertDialogPrimitive.Description.displayName; const AlertDialogAction = React.forwardRef< - React.ElementRef<typeof AlertDialogPrimitive.Action>, - React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> + React.ElementRef<typeof AlertDialogPrimitive.Action>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> >(({ className, ...props }, ref) => ( - <AlertDialogPrimitive.Action - ref={ref} - className={cn(buttonVariants(), className)} - {...props} - /> + <AlertDialogPrimitive.Action + ref={ref} + className={cn(buttonVariants(), className)} + {...props} + /> )); AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; const AlertDialogCancel = React.forwardRef< - React.ElementRef<typeof AlertDialogPrimitive.Cancel>, - React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> + React.ElementRef<typeof AlertDialogPrimitive.Cancel>, + React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> >(({ className, ...props }, ref) => ( - <AlertDialogPrimitive.Cancel - ref={ref} - className={cn( - buttonVariants({ variant: "outline" }), - "mt-2 sm:mt-0", - className - )} - {...props} - /> + <AlertDialogPrimitive.Cancel + ref={ref} + className={cn( + buttonVariants({ variant: "outline" }), + "mt-2 sm:mt-0", + className, + )} + {...props} + /> )); AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, }; diff --git a/apps/web/src/components/ui/alert.tsx b/apps/web/src/components/ui/alert.tsx index cef19199..e941ca43 100644 --- a/apps/web/src/components/ui/alert.tsx +++ b/apps/web/src/components/ui/alert.tsx @@ -1,58 +1,65 @@ import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "../../lib/utils"; +import { AlertTriangle } from "lucide-react"; +import { cn } from "@/utils/ui"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Alert02Icon } from "@hugeicons/core-free-icons"; const alertVariants = cva( - "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - }, - }, - defaultVariants: { - variant: "default", - }, - } + "relative w-full rounded-lg border p-5 py-4.5 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-destructive [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "bg-destructive/5 border-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + }, ); const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> ->(({ className, variant, ...props }, ref) => ( - <div - ref={ref} - role="alert" - className={cn(alertVariants({ variant }), className)} - {...props} - /> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> +>(({ className, variant, children, ...props }, ref) => ( + <div + ref={ref} + role="alert" + className={cn(alertVariants({ variant }), className)} + {...props} + > + {variant === "destructive" && ( + <HugeiconsIcon icon={Alert02Icon} className="size-5 text-destructive mt-0.5" /> + )} + {children} + </div> )); Alert.displayName = "Alert"; const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLHeadingElement> + HTMLParagraphElement, + React.HTMLAttributes<HTMLHeadingElement> >(({ className, ...props }, ref) => ( - <h5 - ref={ref} - className={cn("mb-1 font-medium leading-none tracking-tight", className)} - {...props} - /> + <h5 + ref={ref} + className={cn("mb-2 text-base leading-none font-semibold tracking-tight", className)} + {...props} + /> )); AlertTitle.displayName = "AlertTitle"; const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLParagraphElement> + HTMLParagraphElement, + React.HTMLAttributes<HTMLParagraphElement> >(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("text-sm [&_p]:leading-relaxed", className)} - {...props} - /> + <div + ref={ref} + className={cn("text-sm [&_p]:leading-relaxed", className)} + {...props} + /> )); AlertDescription.displayName = "AlertDescription"; diff --git a/apps/web/src/components/ui/audio-player.tsx b/apps/web/src/components/ui/audio-player.tsx deleted file mode 100644 index 37bf8ca9..00000000 --- a/apps/web/src/components/ui/audio-player.tsx +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; - -import { useRef, useEffect } from "react"; -import { usePlaybackStore } from "@/stores/playback-store"; - -interface AudioPlayerProps { - src: string; - className?: string; - clipStartTime: number; - trimStart: number; - trimEnd: number; - clipDuration: number; - trackMuted?: boolean; -} - -export function AudioPlayer({ - src, - className = "", - clipStartTime, - trimStart, - trimEnd, - clipDuration, - trackMuted = false, -}: AudioPlayerProps) { - const audioRef = useRef<HTMLAudioElement>(null); - const { isPlaying, currentTime, volume, speed, muted } = usePlaybackStore(); - - // Calculate if we're within this clip's timeline range - const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd); - const isInClipRange = - currentTime >= clipStartTime && currentTime < clipEndTime; - - // Sync playback events - useEffect(() => { - const audio = audioRef.current; - if (!audio || !isInClipRange) return; - - const handleSeekEvent = (e: CustomEvent) => { - // Always update audio time, even if outside clip range - const timelineTime = e.detail.time; - const audioTime = Math.max( - trimStart, - Math.min( - clipDuration - trimEnd, - timelineTime - clipStartTime + trimStart - ) - ); - audio.currentTime = audioTime; - }; - - const handleUpdateEvent = (e: CustomEvent) => { - // Always update audio time, even if outside clip range - const timelineTime = e.detail.time; - const targetTime = Math.max( - trimStart, - Math.min( - clipDuration - trimEnd, - timelineTime - clipStartTime + trimStart - ) - ); - - if (Math.abs(audio.currentTime - targetTime) > 0.5) { - audio.currentTime = targetTime; - } - }; - - const handleSpeed = (e: CustomEvent) => { - audio.playbackRate = e.detail.speed; - }; - - window.addEventListener("playback-seek", handleSeekEvent as EventListener); - window.addEventListener( - "playback-update", - handleUpdateEvent as EventListener - ); - window.addEventListener("playback-speed", handleSpeed as EventListener); - - return () => { - window.removeEventListener( - "playback-seek", - handleSeekEvent as EventListener - ); - window.removeEventListener( - "playback-update", - handleUpdateEvent as EventListener - ); - window.removeEventListener( - "playback-speed", - handleSpeed as EventListener - ); - }; - }, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]); - - // Sync playback state - useEffect(() => { - const audio = audioRef.current; - if (!audio) return; - - if (isPlaying && isInClipRange && !trackMuted) { - audio.play().catch(() => {}); - } else { - audio.pause(); - } - }, [isPlaying, isInClipRange, trackMuted]); - - // Sync volume and speed - useEffect(() => { - const audio = audioRef.current; - if (!audio) return; - - audio.volume = volume; - audio.muted = muted || trackMuted; - audio.playbackRate = speed; - }, [volume, speed, muted, trackMuted]); - - return ( - <audio - ref={audioRef} - src={src} - className={className} - preload="auto" - controls={false} - style={{ display: "none" }} // Audio elements don't need visual representation - onContextMenu={(e) => e.preventDefault()} - /> - ); -} diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx index 7979c415..0c2fb4c4 100644 --- a/apps/web/src/components/ui/avatar.tsx +++ b/apps/web/src/components/ui/avatar.tsx @@ -3,47 +3,47 @@ import * as React from "react"; import { Avatar as AvatarPrimitive } from "radix-ui"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Avatar = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> + React.ElementRef<typeof AvatarPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> >(({ className, ...props }, ref) => ( - <AvatarPrimitive.Root - ref={ref} - className={cn( - "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", - className - )} - {...props} - /> + <AvatarPrimitive.Root + ref={ref} + className={cn( + "relative flex size-10 shrink-0 overflow-hidden rounded-full", + className, + )} + {...props} + /> )); Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Image>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> + React.ElementRef<typeof AvatarPrimitive.Image>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> >(({ className, ...props }, ref) => ( - <AvatarPrimitive.Image - ref={ref} - className={cn("aspect-square h-full w-full", className)} - {...props} - /> + <AvatarPrimitive.Image + ref={ref} + className={cn("aspect-square size-full", className)} + {...props} + /> )); AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Fallback>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> + React.ElementRef<typeof AvatarPrimitive.Fallback>, + React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> >(({ className, ...props }, ref) => ( - <AvatarPrimitive.Fallback - ref={ref} - className={cn( - "flex h-full w-full items-center justify-center rounded-full bg-muted", - className - )} - {...props} - /> + <AvatarPrimitive.Fallback + ref={ref} + className={cn( + "bg-muted flex size-full items-center justify-center rounded-full", + className, + )} + {...props} + /> )); AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx index 02b8532b..21240ba0 100644 --- a/apps/web/src/components/ui/badge.tsx +++ b/apps/web/src/components/ui/badge.tsx @@ -1,36 +1,36 @@ -import * as React from "react"; +import type * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, ); export interface BadgeProps - extends React.HTMLAttributes<HTMLDivElement>, - VariantProps<typeof badgeVariants> {} + extends React.HTMLAttributes<HTMLDivElement>, + VariantProps<typeof badgeVariants> {} function Badge({ className, variant, ...props }: BadgeProps) { - return ( - <div className={cn(badgeVariants({ variant }), className)} {...props} /> - ); + return ( + <div className={cn(badgeVariants({ variant }), className)} {...props} /> + ); } export { Badge, badgeVariants }; diff --git a/apps/web/src/components/ui/breadcrumb.tsx b/apps/web/src/components/ui/breadcrumb.tsx index d9df9bf5..e95f2965 100644 --- a/apps/web/src/components/ui/breadcrumb.tsx +++ b/apps/web/src/components/ui/breadcrumb.tsx @@ -2,114 +2,114 @@ import * as React from "react"; import { Slot as SlotPrimitive } from "radix-ui"; import { ChevronRight, MoreHorizontal } from "lucide-react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Breadcrumb = React.forwardRef< - HTMLElement, - React.ComponentPropsWithoutRef<"nav"> & { - separator?: React.ReactNode; - } + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode; + } >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />); Breadcrumb.displayName = "Breadcrumb"; const BreadcrumbList = React.forwardRef< - HTMLOListElement, - React.ComponentPropsWithoutRef<"ol"> + HTMLOListElement, + React.ComponentPropsWithoutRef<"ol"> >(({ className, ...props }, ref) => ( - <ol - ref={ref} - className={cn( - "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", - className - )} - {...props} - /> + <ol + ref={ref} + className={cn( + "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5", + className, + )} + {...props} + /> )); BreadcrumbList.displayName = "BreadcrumbList"; const BreadcrumbItem = React.forwardRef< - HTMLLIElement, - React.ComponentPropsWithoutRef<"li"> + HTMLLIElement, + React.ComponentPropsWithoutRef<"li"> >(({ className, ...props }, ref) => ( - <li - ref={ref} - className={cn("inline-flex items-center gap-1.5", className)} - {...props} - /> + <li + ref={ref} + className={cn("inline-flex items-center gap-1.5", className)} + {...props} + /> )); BreadcrumbItem.displayName = "BreadcrumbItem"; const BreadcrumbLink = React.forwardRef< - HTMLAnchorElement, - React.ComponentPropsWithoutRef<"a"> & { - asChild?: boolean; - } + HTMLAnchorElement, + React.ComponentPropsWithoutRef<"a"> & { + asChild?: boolean; + } >(({ asChild, className, ...props }, ref) => { - const Comp = asChild ? SlotPrimitive.Slot : "a"; + const Comp = asChild ? SlotPrimitive.Slot : "a"; - return ( - <Comp - ref={ref} - className={cn("transition-colors hover:text-foreground", className)} - {...props} - /> - ); + return ( + <Comp + ref={ref} + className={cn("hover:text-foreground", className)} + {...props} + /> + ); }); BreadcrumbLink.displayName = "BreadcrumbLink"; const BreadcrumbPage = React.forwardRef< - HTMLSpanElement, - React.ComponentPropsWithoutRef<"span"> + HTMLSpanElement, + React.ComponentPropsWithoutRef<"span"> >(({ className, ...props }, ref) => ( - <span - ref={ref} - role="link" - aria-disabled="true" - aria-current="page" - className={cn("font-normal text-foreground", className)} - {...props} - /> + <span + ref={ref} + role="link" + aria-disabled="true" + aria-current="page" + className={cn("text-foreground font-normal", className)} + {...props} + /> )); BreadcrumbPage.displayName = "BreadcrumbPage"; const BreadcrumbSeparator = ({ - children, - className, - ...props + children, + className, + ...props }: React.ComponentProps<"li">) => ( - <li - role="presentation" - aria-hidden="true" - className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)} - {...props} - > - {children ?? <ChevronRight />} - </li> + <li + role="presentation" + aria-hidden="true" + className={cn("[&>svg]:size-3.5", className)} + {...props} + > + {children ?? <ChevronRight />} + </li> ); BreadcrumbSeparator.displayName = "BreadcrumbSeparator"; const BreadcrumbEllipsis = ({ - className, - ...props + className, + ...props }: React.ComponentProps<"span">) => ( - <span - role="presentation" - aria-hidden="true" - className={cn("flex h-9 w-9 items-center justify-center", className)} - {...props} - > - <MoreHorizontal className="h-4 w-4" /> - <span className="sr-only">More</span> - </span> + <span + role="presentation" + aria-hidden="true" + className={cn("flex size-9 items-center justify-center", className)} + {...props} + > + <MoreHorizontal className="size-4" /> + <span className="sr-only">More</span> + </span> ); BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"; export { - Breadcrumb, - BreadcrumbList, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbPage, - BreadcrumbSeparator, - BreadcrumbEllipsis, + Breadcrumb, + BreadcrumbList, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbPage, + BreadcrumbSeparator, + BreadcrumbEllipsis, }; diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx index 56a10f44..6c063eff 100644 --- a/apps/web/src/components/ui/button.tsx +++ b/apps/web/src/components/ui/button.tsx @@ -2,59 +2,63 @@ import * as React from "react"; import { Slot as SlotPrimitive } from "radix-ui"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const buttonVariants = cva( - "inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - { - variants: { - variant: { - default: - "bg-foreground text-background shadow-sm hover:bg-foreground/90", - primary: - "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90", - "primary-gradient": - "bg-gradient-to-r from-cyan-400 to-blue-500 text-white hover:opacity-85 transition-opacity", - destructive: - "bg-destructive/0 border border-destructive/25 text-destructive shadow-xs hover:bg-destructive hover:text-destructive-foreground", - outline: - "border border-input bg-transparent shadow-xs hover:opacity-75 transition-opacity hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-foreground/15 border border-input", - text: "bg-transparent p-0 rounded-none opacity-100 hover:opacity-50 transition-opacity", // Instead of ghost (matches app better) - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-sm px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-7 w-7", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } + "inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground hover:bg-primary-hover", + background: + "bg-background text-foreground hover:bg-background/90", + foreground: + "bg-foreground text-background hover:bg-foreground/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/80", + "destructive-foreground": + "border bg-background hover:bg-destructive/15 text-destructive", + outline: + "border border-border bg-transparent hover:bg-accent/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-foreground/15 border border-input", + text: "bg-transparent rounded-none opacity-100 hover:opacity-75", + link: "text-primary underline-offset-4 hover:underline !p-0 !h-auto", + }, + size: { + default: "h-9.5 px-4 py-2", + sm: "h-8 px-3 text-xs", + lg: "h-10 p-5 px-6", + icon: "size-7", + text: "p-0", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, ); export interface ButtonProps - extends React.ButtonHTMLAttributes<HTMLButtonElement>, - VariantProps<typeof buttonVariants> { - asChild?: boolean; + extends React.ButtonHTMLAttributes<HTMLButtonElement>, + VariantProps<typeof buttonVariants> { + asChild?: boolean; } const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? SlotPrimitive.Slot : "button"; - return ( - <Comp - className={cn(buttonVariants({ variant, size, className }))} - ref={ref} - {...props} - /> - ); - } + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? SlotPrimitive.Slot : "button"; + const effectiveSize = size ?? (variant === "text" ? "text" : "default"); + return ( + <Comp + className={cn(buttonVariants({ variant, size: effectiveSize, className }))} + ref={ref} + {...props} + /> + ); + }, ); Button.displayName = "Button"; diff --git a/apps/web/src/components/ui/calendar.tsx b/apps/web/src/components/ui/calendar.tsx index 192643cb..a0664cc3 100644 --- a/apps/web/src/components/ui/calendar.tsx +++ b/apps/web/src/components/ui/calendar.tsx @@ -1,71 +1,71 @@ "use client"; -import * as React from "react"; +import type * as React from "react"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { DayPicker } from "react-day-picker"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; import { buttonVariants } from "./button"; export type CalendarProps = React.ComponentProps<typeof DayPicker>; function Calendar({ - className, - classNames, - showOutsideDays = true, - ...props + className, + classNames, + showOutsideDays = true, + ...props }: CalendarProps) { - return ( - <DayPicker - showOutsideDays={showOutsideDays} - className={cn("p-3", className)} - classNames={{ - months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", - month: "space-y-4", - caption: "flex justify-center pt-1 relative items-center", - caption_label: "text-sm font-medium", - nav: "space-x-1 flex items-center", - nav_button: cn( - buttonVariants({ variant: "outline" }), - "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100" - ), - nav_button_previous: "absolute left-1", - nav_button_next: "absolute right-1", - table: "w-full border-collapse space-y-1", - head_row: "flex", - head_cell: - "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]", - row: "flex w-full mt-2", - cell: cn( - "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md", - props.mode === "range" - ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" - : "[&:has([aria-selected])]:rounded-md" - ), - day: cn( - buttonVariants({ variant: "text" }), - "h-8 w-8 p-0 font-normal aria-selected:opacity-100" - ), - day_range_start: "day-range-start", - day_range_end: "day-range-end", - day_selected: - "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", - day_today: "bg-accent text-accent-foreground", - day_outside: - "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground", - day_disabled: "text-muted-foreground opacity-50", - day_range_middle: - "aria-selected:bg-accent aria-selected:text-accent-foreground", - day_hidden: "invisible", - ...classNames, - }} - components={{ - IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />, - IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />, - }} - {...props} - /> - ); + return ( + <DayPicker + showOutsideDays={showOutsideDays} + className={cn("p-3", className)} + classNames={{ + months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", + month: "space-y-4", + caption: "flex justify-center pt-1 relative items-center", + caption_label: "text-sm font-medium", + nav: "space-x-1 flex items-center", + nav_button: cn( + buttonVariants({ variant: "outline" }), + "size-7 bg-transparent p-0 opacity-50 hover:opacity-100", + ), + nav_button_previous: "absolute left-1", + nav_button_next: "absolute right-1", + table: "w-full border-collapse space-y-1", + head_row: "flex", + head_cell: + "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]", + row: "flex w-full mt-2", + cell: cn( + "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md", + props.mode === "range" + ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" + : "[&:has([aria-selected])]:rounded-md", + ), + day: cn( + buttonVariants({ variant: "text" }), + "size-8 p-0 font-normal aria-selected:opacity-100", + ), + day_range_start: "day-range-start", + day_range_end: "day-range-end", + day_selected: + "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", + day_today: "bg-accent text-accent-foreground", + day_outside: + "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground", + day_disabled: "text-muted-foreground opacity-50", + day_range_middle: + "aria-selected:bg-accent aria-selected:text-accent-foreground", + day_hidden: "invisible", + ...classNames, + }} + components={{ + IconLeft: ({ ...props }) => <ChevronLeft className="size-4" />, + IconRight: ({ ...props }) => <ChevronRight className="size-4" />, + }} + {...props} + /> + ); } Calendar.displayName = "Calendar"; diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx index 6d0b37cc..c3fc45f0 100644 --- a/apps/web/src/components/ui/card.tsx +++ b/apps/web/src/components/ui/card.tsx @@ -1,80 +1,80 @@ import * as React from "react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("rounded-xl border bg-card text-card-foreground", className)} - {...props} - /> + <div + ref={ref} + className={cn("bg-card text-card-foreground rounded-2xl border", className)} + {...props} + /> )); Card.displayName = "Card"; const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("flex flex-col space-y-1.5 p-6", className)} - {...props} - /> + <div + ref={ref} + className={cn("flex flex-col space-y-1.5 p-6", className)} + {...props} + /> )); CardHeader.displayName = "CardHeader"; const CardTitle = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("font-semibold leading-none tracking-tight", className)} - {...props} - /> + <div + ref={ref} + className={cn("leading-none font-semibold tracking-tight", className)} + {...props} + /> )); CardTitle.displayName = "CardTitle"; const CardDescription = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("text-sm text-muted-foreground", className)} - {...props} - /> + <div + ref={ref} + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> )); CardDescription.displayName = "CardDescription"; const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => ( - <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> + <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> )); CardContent.displayName = "CardContent"; const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("flex items-center p-6 pt-0", className)} - {...props} - /> + <div + ref={ref} + className={cn("flex items-center p-6 pt-0", className)} + {...props} + /> )); CardFooter.displayName = "CardFooter"; export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardDescription, - CardContent, + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, }; diff --git a/apps/web/src/components/ui/carousel.tsx b/apps/web/src/components/ui/carousel.tsx index a9c98b67..9018b9ea 100644 --- a/apps/web/src/components/ui/carousel.tsx +++ b/apps/web/src/components/ui/carousel.tsx @@ -1,12 +1,12 @@ "use client"; -import * as React from "react"; import useEmblaCarousel, { - type UseEmblaCarouselType, + type UseEmblaCarouselType, } from "embla-carousel-react"; import { ArrowLeft, ArrowRight } from "lucide-react"; +import * as React from "react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; import { Button } from "./button"; type CarouselApi = UseEmblaCarouselType[1]; @@ -15,248 +15,248 @@ type CarouselOptions = UseCarouselParameters[0]; type CarouselPlugin = UseCarouselParameters[1]; type CarouselProps = { - opts?: CarouselOptions; - plugins?: CarouselPlugin; - orientation?: "horizontal" | "vertical"; - setApi?: (api: CarouselApi) => void; + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: "horizontal" | "vertical"; + setApi?: (api: CarouselApi) => void; }; type CarouselContextProps = { - carouselRef: ReturnType<typeof useEmblaCarousel>[0]; - api: ReturnType<typeof useEmblaCarousel>[1]; - scrollPrev: () => void; - scrollNext: () => void; - canScrollPrev: boolean; - canScrollNext: boolean; + carouselRef: ReturnType<typeof useEmblaCarousel>[0]; + api: ReturnType<typeof useEmblaCarousel>[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; } & CarouselProps; const CarouselContext = React.createContext<CarouselContextProps | null>(null); function useCarousel() { - const context = React.useContext(CarouselContext); + const context = React.useContext(CarouselContext); - if (!context) { - throw new Error("useCarousel must be used within a <Carousel />"); - } + if (!context) { + throw new Error("useCarousel must be used within a <Carousel />"); + } - return context; + return context; } const Carousel = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> & CarouselProps + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> & CarouselProps >( - ( - { - orientation = "horizontal", - opts, - setApi, - plugins, - className, - children, - ...props - }, - ref - ) => { - const [carouselRef, api] = useEmblaCarousel( - { - ...opts, - axis: orientation === "horizontal" ? "x" : "y", - }, - plugins - ); - const [canScrollPrev, setCanScrollPrev] = React.useState(false); - const [canScrollNext, setCanScrollNext] = React.useState(false); + ( + { + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + ...props + }, + ref, + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins, + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); - const onSelect = React.useCallback((api: CarouselApi) => { - if (!api) { - return; - } + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return; + } - setCanScrollPrev(api.canScrollPrev()); - setCanScrollNext(api.canScrollNext()); - }, []); + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); - const scrollPrev = React.useCallback(() => { - api?.scrollPrev(); - }, [api]); + const scrollPrev = React.useCallback(() => { + api?.scrollPrev(); + }, [api]); - const scrollNext = React.useCallback(() => { - api?.scrollNext(); - }, [api]); + const scrollNext = React.useCallback(() => { + api?.scrollNext(); + }, [api]); - const handleKeyDown = React.useCallback( - (event: React.KeyboardEvent<HTMLDivElement>) => { - if (event.key === "ArrowLeft") { - event.preventDefault(); - scrollPrev(); - } else if (event.key === "ArrowRight") { - event.preventDefault(); - scrollNext(); - } - }, - [scrollPrev, scrollNext] - ); + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent<HTMLDivElement>) => { + if (event.key === "ArrowLeft") { + event.preventDefault(); + scrollPrev(); + } else if (event.key === "ArrowRight") { + event.preventDefault(); + scrollNext(); + } + }, + [scrollPrev, scrollNext], + ); - React.useEffect(() => { - if (!api || !setApi) { - return; - } + React.useEffect(() => { + if (!api || !setApi) { + return; + } - setApi(api); - }, [api, setApi]); + setApi(api); + }, [api, setApi]); - React.useEffect(() => { - if (!api) { - return; - } + React.useEffect(() => { + if (!api) { + return; + } - onSelect(api); - api.on("reInit", onSelect); - api.on("select", onSelect); + onSelect(api); + api.on("reInit", onSelect); + api.on("select", onSelect); - return () => { - api?.off("select", onSelect); - }; - }, [api, onSelect]); + return () => { + api?.off("select", onSelect); + }; + }, [api, onSelect]); - return ( - <CarouselContext.Provider - value={{ - carouselRef, - api, - opts, - orientation: - orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), - scrollPrev, - scrollNext, - canScrollPrev, - canScrollNext, - }} - > - <div - ref={ref} - onKeyDownCapture={handleKeyDown} - className={cn("relative", className)} - role="region" - aria-roledescription="carousel" - {...props} - > - {children} - </div> - </CarouselContext.Provider> - ); - } + return ( + <CarouselContext.Provider + value={{ + carouselRef, + api, + opts, + orientation: + orientation || (opts?.axis === "y" ? "vertical" : "horizontal"), + scrollPrev, + scrollNext, + canScrollPrev, + canScrollNext, + }} + > + <div + ref={ref} + onKeyDownCapture={handleKeyDown} + className={cn("relative", className)} + role="region" + aria-roledescription="carousel" + {...props} + > + {children} + </div> + </CarouselContext.Provider> + ); + }, ); Carousel.displayName = "Carousel"; const CarouselContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => { - const { carouselRef, orientation } = useCarousel(); + const { carouselRef, orientation } = useCarousel(); - return ( - <div ref={carouselRef} className="overflow-hidden"> - <div - ref={ref} - className={cn( - "flex", - orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", - className - )} - {...props} - /> - </div> - ); + return ( + <div ref={carouselRef} className="overflow-hidden"> + <div + ref={ref} + className={cn( + "flex", + orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", + className, + )} + {...props} + /> + </div> + ); }); CarouselContent.displayName = "CarouselContent"; const CarouselItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => { - const { orientation } = useCarousel(); + const { orientation } = useCarousel(); - return ( - <div - ref={ref} - role="group" - aria-roledescription="slide" - className={cn( - "min-w-0 shrink-0 grow-0 basis-full", - orientation === "horizontal" ? "pl-4" : "pt-4", - className - )} - {...props} - /> - ); + return ( + <div + ref={ref} + role="group" + aria-roledescription="slide" + className={cn( + "min-w-0 shrink-0 grow-0 basis-full", + orientation === "horizontal" ? "pl-4" : "pt-4", + className, + )} + {...props} + /> + ); }); CarouselItem.displayName = "CarouselItem"; const CarouselPrevious = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<typeof Button> + HTMLButtonElement, + React.ComponentProps<typeof Button> >(({ className, variant = "outline", size = "icon", ...props }, ref) => { - const { orientation, scrollPrev, canScrollPrev } = useCarousel(); + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); - return ( - <Button - ref={ref} - variant={variant} - size={size} - className={cn( - "absolute h-8 w-8 rounded-full", - orientation === "horizontal" - ? "-left-12 top-1/2 -translate-y-1/2" - : "-top-12 left-1/2 -translate-x-1/2 rotate-90", - className - )} - disabled={!canScrollPrev} - onClick={scrollPrev} - {...props} - > - <ArrowLeft className="h-4 w-4" /> - <span className="sr-only">Previous slide</span> - </Button> - ); + return ( + <Button + ref={ref} + variant={variant} + size={size} + className={cn( + "absolute size-8 rounded-full", + orientation === "horizontal" + ? "top-1/2 -left-12 -translate-y-1/2" + : "-top-12 left-1/2 -translate-x-1/2 rotate-90", + className, + )} + disabled={!canScrollPrev} + onClick={scrollPrev} + {...props} + > + <ArrowLeft className="size-4" /> + <span className="sr-only">Previous slide</span> + </Button> + ); }); CarouselPrevious.displayName = "CarouselPrevious"; const CarouselNext = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<typeof Button> + HTMLButtonElement, + React.ComponentProps<typeof Button> >(({ className, variant = "outline", size = "icon", ...props }, ref) => { - const { orientation, scrollNext, canScrollNext } = useCarousel(); + const { orientation, scrollNext, canScrollNext } = useCarousel(); - return ( - <Button - ref={ref} - variant={variant} - size={size} - className={cn( - "absolute h-8 w-8 rounded-full", - orientation === "horizontal" - ? "-right-12 top-1/2 -translate-y-1/2" - : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", - className - )} - disabled={!canScrollNext} - onClick={scrollNext} - {...props} - > - <ArrowRight className="h-4 w-4" /> - <span className="sr-only">Next slide</span> - </Button> - ); + return ( + <Button + ref={ref} + variant={variant} + size={size} + className={cn( + "absolute size-8 rounded-full", + orientation === "horizontal" + ? "top-1/2 -right-12 -translate-y-1/2" + : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", + className, + )} + disabled={!canScrollNext} + onClick={scrollNext} + {...props} + > + <ArrowRight className="size-4" /> + <span className="sr-only">Next slide</span> + </Button> + ); }); CarouselNext.displayName = "CarouselNext"; export { - type CarouselApi, - Carousel, - CarouselContent, - CarouselItem, - CarouselPrevious, - CarouselNext, + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, }; diff --git a/apps/web/src/components/ui/chart.tsx b/apps/web/src/components/ui/chart.tsx index a469c87a..2bd04b20 100644 --- a/apps/web/src/components/ui/chart.tsx +++ b/apps/web/src/components/ui/chart.tsx @@ -3,368 +3,368 @@ import * as React from "react"; import * as RechartsPrimitive from "recharts"; import { - NameType, - Payload, - ValueType, + NameType, + Payload, + ValueType, } from "recharts/types/component/DefaultTooltipContent"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; // Format: { THEME_NAME: CSS_SELECTOR } const THEMES = { light: "", dark: ".dark" } as const; export type ChartConfig = { - [k in string]: { - label?: React.ReactNode; - icon?: React.ComponentType; - } & ( - | { color?: string; theme?: never } - | { color?: never; theme: Record<keyof typeof THEMES, string> } - ); + [k in string]: { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record<keyof typeof THEMES, string> } + ); }; type ChartContextProps = { - config: ChartConfig; + config: ChartConfig; }; const ChartContext = React.createContext<ChartContextProps | null>(null); function useChart() { - const context = React.useContext(ChartContext); + const context = React.useContext(ChartContext); - if (!context) { - throw new Error("useChart must be used within a <ChartContainer />"); - } + if (!context) { + throw new Error("useChart must be used within a <ChartContainer />"); + } - return context; + return context; } const ChartContainer = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - config: ChartConfig; - children: React.ComponentProps< - typeof RechartsPrimitive.ResponsiveContainer - >["children"]; - } + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig; + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"]; + } >(({ id, className, children, config, ...props }, ref) => { - const uniqueId = React.useId(); - const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; - return ( - <ChartContext.Provider value={{ config }}> - <div - data-chart={chartId} - ref={ref} - className={cn( - "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden", - className - )} - {...props} - > - <ChartStyle id={chartId} config={config} /> - <RechartsPrimitive.ResponsiveContainer> - {children} - </RechartsPrimitive.ResponsiveContainer> - </div> - </ChartContext.Provider> - ); + return ( + <ChartContext.Provider value={{ config }}> + <div + data-chart={chartId} + ref={ref} + className={cn( + "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden", + className, + )} + {...props} + > + <ChartStyle id={chartId} config={config} /> + <RechartsPrimitive.ResponsiveContainer> + {children} + </RechartsPrimitive.ResponsiveContainer> + </div> + </ChartContext.Provider> + ); }); ChartContainer.displayName = "Chart"; const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { - const colorConfig = Object.entries(config).filter( - ([_, config]) => config.theme || config.color - ); + const colorConfig = Object.entries(config).filter( + ([_, config]) => config.theme || config.color, + ); - if (!colorConfig.length) { - return null; - } + if (!colorConfig.length) { + return null; + } - return ( - <style - dangerouslySetInnerHTML={{ - __html: Object.entries(THEMES) - .map( - ([theme, prefix]) => ` + return ( + <style + dangerouslySetInnerHTML={{ + __html: Object.entries(THEMES) + .map( + ([theme, prefix]) => ` ${prefix} [data-chart=${id}] { ${colorConfig - .map(([key, itemConfig]) => { - const color = - itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || - itemConfig.color; - return color ? ` --color-${key}: ${color};` : null; - }) - .join("\n")} + .map(([key, itemConfig]) => { + const color = + itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || + itemConfig.color; + return color ? ` --color-${key}: ${color};` : null; + }) + .join("\n")} } -` - ) - .join("\n"), - }} - /> - ); +`, + ) + .join("\n"), + }} + /> + ); }; const ChartTooltip = RechartsPrimitive.Tooltip; const ChartTooltipContent = React.forwardRef< - HTMLDivElement, - React.ComponentProps<typeof RechartsPrimitive.Tooltip> & - React.ComponentProps<"div"> & { - hideLabel?: boolean; - hideIndicator?: boolean; - indicator?: "line" | "dot" | "dashed"; - nameKey?: string; - labelKey?: string; - } + HTMLDivElement, + React.ComponentProps<typeof RechartsPrimitive.Tooltip> & + React.ComponentProps<"div"> & { + hideLabel?: boolean; + hideIndicator?: boolean; + indicator?: "line" | "dot" | "dashed"; + nameKey?: string; + labelKey?: string; + } >( - ( - { - active, - payload, - className, - indicator = "dot", - hideLabel = false, - hideIndicator = false, - label, - labelFormatter, - labelClassName, - formatter, - color, - nameKey, - labelKey, - }, - ref - ) => { - const { config } = useChart(); + ( + { + active, + payload, + className, + indicator = "dot", + hideLabel = false, + hideIndicator = false, + label, + labelFormatter, + labelClassName, + formatter, + color, + nameKey, + labelKey, + }, + ref, + ) => { + const { config } = useChart(); - const tooltipLabel = React.useMemo(() => { - if (hideLabel || !payload?.length) { - return null; - } + const tooltipLabel = React.useMemo(() => { + if (hideLabel || !payload?.length) { + return null; + } - const [item] = payload; - const key = `${labelKey || item.dataKey || item.name || "value"}`; - const itemConfig = getPayloadConfigFromPayload(config, item, key); - const value = - !labelKey && typeof label === "string" - ? config[label as keyof typeof config]?.label || label - : itemConfig?.label; + const [item] = payload; + const key = `${labelKey || item.dataKey || item.name || "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + const value = + !labelKey && typeof label === "string" + ? config[label as keyof typeof config]?.label || label + : itemConfig?.label; - if (labelFormatter) { - return ( - <div className={cn("font-medium", labelClassName)}> - {labelFormatter(value, payload)} - </div> - ); - } + if (labelFormatter) { + return ( + <div className={cn("font-medium", labelClassName)}> + {labelFormatter(value, payload)} + </div> + ); + } - if (!value) { - return null; - } + if (!value) { + return null; + } - return <div className={cn("font-medium", labelClassName)}>{value}</div>; - }, [ - label, - labelFormatter, - payload, - hideLabel, - labelClassName, - config, - labelKey, - ]); + return <div className={cn("font-medium", labelClassName)}>{value}</div>; + }, [ + label, + labelFormatter, + payload, + hideLabel, + labelClassName, + config, + labelKey, + ]); - if (!active || !payload?.length) { - return null; - } + if (!active || !payload?.length) { + return null; + } - const nestLabel = payload.length === 1 && indicator !== "dot"; + const nestLabel = payload.length === 1 && indicator !== "dot"; - return ( - <div - ref={ref} - className={cn( - "grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl", - className - )} - > - {nestLabel ? null : tooltipLabel} - <div className="grid gap-1.5"> - {payload.map((item, index) => { - const key = `${nameKey || item.name || item.dataKey || "value"}`; - const itemConfig = getPayloadConfigFromPayload(config, item, key); - const indicatorColor = color || item.payload.fill || item.color; + return ( + <div + ref={ref} + className={cn( + "border-border/50 bg-background grid min-w-32 items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl", + className, + )} + > + {nestLabel ? null : tooltipLabel} + <div className="grid gap-1.5"> + {payload.map((item, index) => { + const key = `${nameKey || item.name || item.dataKey || "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + const indicatorColor = color || item.payload.fill || item.color; - return ( - <div - key={item.dataKey} - className={cn( - "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", - indicator === "dot" && "items-center" - )} - > - {formatter && item?.value !== undefined && item.name ? ( - formatter(item.value, item.name, item, index, item.payload) - ) : ( - <> - {itemConfig?.icon ? ( - <itemConfig.icon /> - ) : ( - !hideIndicator && ( - <div - className={cn( - "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)", - { - "h-2.5 w-2.5": indicator === "dot", - "w-1": indicator === "line", - "w-0 border-[1.5px] border-dashed bg-transparent": - indicator === "dashed", - "my-0.5": nestLabel && indicator === "dashed", - } - )} - style={ - { - "--color-bg": indicatorColor, - "--color-border": indicatorColor, - } as React.CSSProperties - } - /> - ) - )} - <div - className={cn( - "flex flex-1 justify-between leading-none", - nestLabel ? "items-end" : "items-center" - )} - > - <div className="grid gap-1.5"> - {nestLabel ? tooltipLabel : null} - <span className="text-muted-foreground"> - {itemConfig?.label || item.name} - </span> - </div> - {item.value && ( - <span className="font-mono font-medium tabular-nums text-foreground"> - {item.value.toLocaleString()} - </span> - )} - </div> - </> - )} - </div> - ); - })} - </div> - </div> - ); - } + return ( + <div + key={item.dataKey} + className={cn( + "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5", + indicator === "dot" && "items-center", + )} + > + {formatter && item?.value !== undefined && item.name ? ( + formatter(item.value, item.name, item, index, item.payload) + ) : ( + <> + {itemConfig?.icon ? ( + <itemConfig.icon /> + ) : ( + !hideIndicator && ( + <div + className={cn( + "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)", + { + "size-2.5": indicator === "dot", + "w-1": indicator === "line", + "w-0 border-[1.5px] border-dashed bg-transparent": + indicator === "dashed", + "my-0.5": nestLabel && indicator === "dashed", + }, + )} + style={ + { + "--color-bg": indicatorColor, + "--color-border": indicatorColor, + } as React.CSSProperties + } + /> + ) + )} + <div + className={cn( + "flex flex-1 justify-between leading-none", + nestLabel ? "items-end" : "items-center", + )} + > + <div className="grid gap-1.5"> + {nestLabel ? tooltipLabel : null} + <span className="text-muted-foreground"> + {itemConfig?.label || item.name} + </span> + </div> + {item.value && ( + <span className="text-foreground font-mono font-medium tabular-nums"> + {item.value.toLocaleString()} + </span> + )} + </div> + </> + )} + </div> + ); + })} + </div> + </div> + ); + }, ); ChartTooltipContent.displayName = "ChartTooltip"; const ChartLegend = RechartsPrimitive.Legend; const ChartLegendContent = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & - Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { - hideIcon?: boolean; - nameKey?: string; - } + HTMLDivElement, + React.ComponentProps<"div"> & + Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { + hideIcon?: boolean; + nameKey?: string; + } >( - ( - { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, - ref - ) => { - const { config } = useChart(); + ( + { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, + ref, + ) => { + const { config } = useChart(); - if (!payload?.length) { - return null; - } + if (!payload?.length) { + return null; + } - return ( - <div - ref={ref} - className={cn( - "flex items-center justify-center gap-4", - verticalAlign === "top" ? "pb-3" : "pt-3", - className - )} - > - {payload.map((item) => { - const key = `${nameKey || item.dataKey || "value"}`; - const itemConfig = getPayloadConfigFromPayload(config, item, key); + return ( + <div + ref={ref} + className={cn( + "flex items-center justify-center gap-4", + verticalAlign === "top" ? "pb-3" : "pt-3", + className, + )} + > + {payload.map((item) => { + const key = `${nameKey || item.dataKey || "value"}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); - return ( - <div - key={item.value} - className={cn( - "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground" - )} - > - {itemConfig?.icon && !hideIcon ? ( - <itemConfig.icon /> - ) : ( - <div - className="h-2 w-2 shrink-0 rounded-[2px]" - style={{ - backgroundColor: item.color, - }} - /> - )} - {itemConfig?.label} - </div> - ); - })} - </div> - ); - } + return ( + <div + key={item.value} + className={cn( + "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:size-3", + )} + > + {itemConfig?.icon && !hideIcon ? ( + <itemConfig.icon /> + ) : ( + <div + className="size-2 shrink-0 rounded-[2px]" + style={{ + backgroundColor: item.color, + }} + /> + )} + {itemConfig?.label} + </div> + ); + })} + </div> + ); + }, ); ChartLegendContent.displayName = "ChartLegend"; // Helper to extract item config from a payload. function getPayloadConfigFromPayload( - config: ChartConfig, - payload: unknown, - key: string + config: ChartConfig, + payload: unknown, + key: string, ) { - if (typeof payload !== "object" || payload === null) { - return; - } + if (typeof payload !== "object" || payload === null) { + return; + } - const payloadPayload = - "payload" in payload && - typeof payload.payload === "object" && - payload.payload !== null - ? payload.payload - : undefined; + const payloadPayload = + "payload" in payload && + typeof payload.payload === "object" && + payload.payload !== null + ? payload.payload + : undefined; - let configLabelKey: string = key; + let configLabelKey: string = key; - if ( - key in payload && - typeof payload[key as keyof typeof payload] === "string" - ) { - configLabelKey = payload[key as keyof typeof payload] as string; - } else if ( - payloadPayload && - key in payloadPayload && - typeof payloadPayload[key as keyof typeof payloadPayload] === "string" - ) { - configLabelKey = payloadPayload[ - key as keyof typeof payloadPayload - ] as string; - } + if ( + key in payload && + typeof payload[key as keyof typeof payload] === "string" + ) { + configLabelKey = payload[key as keyof typeof payload] as string; + } else if ( + payloadPayload && + key in payloadPayload && + typeof payloadPayload[key as keyof typeof payloadPayload] === "string" + ) { + configLabelKey = payloadPayload[ + key as keyof typeof payloadPayload + ] as string; + } - return configLabelKey in config - ? config[configLabelKey] - : config[key as keyof typeof config]; + return configLabelKey in config + ? config[configLabelKey] + : config[key as keyof typeof config]; } export { - ChartContainer, - ChartTooltip, - ChartTooltipContent, - ChartLegend, - ChartLegendContent, - ChartStyle, + ChartContainer, + ChartTooltip, + ChartTooltipContent, + ChartLegend, + ChartLegendContent, + ChartStyle, }; diff --git a/apps/web/src/components/ui/checkbox.tsx b/apps/web/src/components/ui/checkbox.tsx index 5e429994..af33b5b3 100644 --- a/apps/web/src/components/ui/checkbox.tsx +++ b/apps/web/src/components/ui/checkbox.tsx @@ -4,26 +4,26 @@ import * as React from "react"; import { Checkbox as CheckboxPrimitive } from "radix-ui"; import { Check } from "lucide-react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Checkbox = React.forwardRef< - React.ElementRef<typeof CheckboxPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> + React.ElementRef<typeof CheckboxPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> >(({ className, ...props }, ref) => ( - <CheckboxPrimitive.Root - ref={ref} - className={cn( - "peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow-sm focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", - className - )} - {...props} - > - <CheckboxPrimitive.Indicator - className={cn("flex items-center justify-center text-current")} - > - <Check className="h-4 w-4" /> - </CheckboxPrimitive.Indicator> - </CheckboxPrimitive.Root> + <CheckboxPrimitive.Root + ref={ref} + className={cn( + "cursor-pointer bg-background peer focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary size-4 shrink-0 shadow-xs rounded-sm border focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", + className, + )} + {...props} + > + <CheckboxPrimitive.Indicator + className={cn("flex items-center justify-center text-current")} + > + <Check className="size-4" /> + </CheckboxPrimitive.Indicator> + </CheckboxPrimitive.Root> )); Checkbox.displayName = CheckboxPrimitive.Root.displayName; diff --git a/apps/web/src/components/ui/color-picker.tsx b/apps/web/src/components/ui/color-picker.tsx index b94c1f98..ad350e0e 100644 --- a/apps/web/src/components/ui/color-picker.tsx +++ b/apps/web/src/components/ui/color-picker.tsx @@ -1,336 +1,343 @@ -import * as React from "react"; +import { forwardRef, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; import { Input } from "./input"; interface ColorPickerProps { - value?: string; - onChange?: (value: string) => void; - className?: string; - containerRef?: React.RefObject<HTMLDivElement>; + value?: string; + onChange?: (value: string) => void; + className?: string; + containerRef?: React.RefObject<HTMLDivElement>; } const hexToHsv = (hex: string) => { - const r = parseInt(hex.slice(0, 2), 16) / 255; - const g = parseInt(hex.slice(2, 4), 16) / 255; - const b = parseInt(hex.slice(4, 6), 16) / 255; + const r = parseInt(hex.slice(0, 2), 16) / 255; + const g = parseInt(hex.slice(2, 4), 16) / 255; + const b = parseInt(hex.slice(4, 6), 16) / 255; - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const diff = max - min; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const diff = max - min; - let h = 0; - let s = max === 0 ? 0 : diff / max; - let v = max; + let h = 0; + const s = max === 0 ? 0 : diff / max; + const v = max; - if (diff !== 0) { - switch (max) { - case r: - h = ((g - b) / diff) % 6; - break; - case g: - h = (b - r) / diff + 2; - break; - case b: - h = (r - g) / diff + 4; - break; - } - } + if (diff !== 0) { + switch (max) { + case r: + h = ((g - b) / diff) % 6; + break; + case g: + h = (b - r) / diff + 2; + break; + case b: + h = (r - g) / diff + 4; + break; + } + } - h = (h * 60 + 360) % 360; - if (isNaN(h)) h = 0; + h = (h * 60 + 360) % 360; + if (Number.isNaN(h)) h = 0; - return [h, s, v]; + return [h, s, v]; }; const hsvToHex = (h: number, s: number, v: number) => { - const c = v * s; - const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); - const m = v - c; + const c = v * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = v - c; - let r = 0, - g = 0, - b = 0; + let r = 0, + g = 0, + b = 0; - if (h >= 0 && h < 60) { - r = c; - g = x; - b = 0; - } else if (h >= 60 && h < 120) { - r = x; - g = c; - b = 0; - } else if (h >= 120 && h < 180) { - r = 0; - g = c; - b = x; - } else if (h >= 180 && h < 240) { - r = 0; - g = x; - b = c; - } else if (h >= 240 && h < 300) { - r = x; - g = 0; - b = c; - } else if (h >= 300 && h < 360) { - r = c; - g = 0; - b = x; - } + if (h >= 0 && h < 60) { + r = c; + g = x; + b = 0; + } else if (h >= 60 && h < 120) { + r = x; + g = c; + b = 0; + } else if (h >= 120 && h < 180) { + r = 0; + g = c; + b = x; + } else if (h >= 180 && h < 240) { + r = 0; + g = x; + b = c; + } else if (h >= 240 && h < 300) { + r = x; + g = 0; + b = c; + } else if (h >= 300 && h < 360) { + r = c; + g = 0; + b = x; + } - r = Math.round((r + m) * 255); - g = Math.round((g + m) * 255); - b = Math.round((b + m) * 255); + r = Math.round((r + m) * 255); + g = Math.round((g + m) * 255); + b = Math.round((b + m) * 255); - return [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join(""); + return [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join(""); }; -const ColorPicker = React.forwardRef<HTMLDivElement, ColorPickerProps>( - ({ className, value = "FFFFFF", onChange, containerRef, ...props }, ref) => { - const [isOpen, setIsOpen] = React.useState(false); - const [isDragging, setIsDragging] = React.useState< - "saturation" | "hue" | null - >(null); - const [pickerPosition, setPickerPosition] = React.useState({ - right: 0, - bottom: 0, - }); - const [internalHue, setInternalHue] = React.useState(0); - const [inputValue, setInputValue] = React.useState(value); +const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>( + ({ className, value = "FFFFFF", onChange, containerRef, ...props }, ref) => { + const [isOpen, setIsOpen] = useState(false); + const [isDragging, setIsDragging] = useState<"saturation" | "hue" | null>( + null, + ); + const [pickerPosition, setPickerPosition] = useState({ + right: 0, + bottom: 0, + }); + const [internalHue, setInternalHue] = useState(0); + const [inputValue, setInputValue] = useState(value); - const pickerRef = React.useRef<HTMLDivElement>(null); - const saturationRef = React.useRef<HTMLDivElement>(null); - const hueRef = React.useRef<HTMLDivElement>(null); - const triggerRef = React.useRef<HTMLDivElement>(null); + const pickerRef = useRef<HTMLDivElement>(null); + const saturationRef = useRef<HTMLButtonElement>(null); + const hueRef = useRef<HTMLButtonElement>(null); + const triggerRef = useRef<HTMLButtonElement>(null); - const [h, s, v] = hexToHsv(value); - const displayHue = s > 0 ? h : internalHue; + const [h, s, v] = hexToHsv(value); + const displayHue = s > 0 ? h : internalHue; - React.useEffect(() => { - setInputValue(value); - }, [value]); + useEffect(() => { + setInputValue(value); + }, [value]); - React.useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - pickerRef.current && - !pickerRef.current.contains(event.target as Node) - ) { - setIsOpen(false); - } - }; + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + pickerRef.current && + !pickerRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; - if (isOpen) { - document.addEventListener("mousedown", handleClickOutside); - return () => - document.removeEventListener("mousedown", handleClickOutside); - } - }, [isOpen]); + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + return () => + document.removeEventListener("mousedown", handleClickOutside); + } + }, [isOpen]); - React.useEffect(() => { - const handleMouseMove = (e: MouseEvent) => { - if (!isDragging) return; + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging) return; - if (isDragging === "saturation" && saturationRef.current) { - const rect = saturationRef.current.getBoundingClientRect(); - const x = Math.max( - 0, - Math.min(1, (e.clientX - rect.left) / rect.width) - ); - const y = Math.max( - 0, - Math.min(1, (e.clientY - rect.top) / rect.height) - ); - const newS = x; - const newV = 1 - y; - const newHex = hsvToHex(displayHue, newS, newV); - onChange?.(newHex); - } + if (isDragging === "saturation" && saturationRef.current) { + const rect = saturationRef.current.getBoundingClientRect(); + const x = Math.max( + 0, + Math.min(1, (e.clientX - rect.left) / rect.width), + ); + const y = Math.max( + 0, + Math.min(1, (e.clientY - rect.top) / rect.height), + ); + const newS = x; + const newV = 1 - y; + const newHex = hsvToHex(displayHue, newS, newV); + onChange?.(newHex); + } - if (isDragging === "hue" && hueRef.current) { - const rect = hueRef.current.getBoundingClientRect(); - const x = Math.max( - 0, - Math.min(1, (e.clientX - rect.left) / rect.width) - ); - const newH = x * 360; - setInternalHue(newH); - if (s > 0) { - const newHex = hsvToHex(newH, s, v); - onChange?.(newHex); - } - } - }; + if (isDragging === "hue" && hueRef.current) { + const rect = hueRef.current.getBoundingClientRect(); + const x = Math.max( + 0, + Math.min(1, (e.clientX - rect.left) / rect.width), + ); + const newH = x * 360; + setInternalHue(newH); + if (s > 0) { + const newHex = hsvToHex(newH, s, v); + onChange?.(newHex); + } + } + }; - const handleMouseUp = () => { - setIsDragging(null); - }; + const handleMouseUp = () => { + setIsDragging(null); + }; - if (isDragging) { - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); - return () => { - document.removeEventListener("mousemove", handleMouseMove); - document.removeEventListener("mouseup", handleMouseUp); - }; - } - }, [isDragging, displayHue, s, v, onChange]); + if (isDragging) { + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + } + }, [isDragging, displayHue, s, v, onChange]); - const handleSaturationMouseDown = (e: React.MouseEvent) => { - e.preventDefault(); - setIsDragging("saturation"); - const rect = saturationRef.current!.getBoundingClientRect(); - const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); - const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height)); - const newS = x; - const newV = 1 - y; - const newHex = hsvToHex(displayHue, newS, newV); - onChange?.(newHex); - }; + const handleSaturationMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + const saturationElement = saturationRef.current; + if (!saturationElement) return; + setIsDragging("saturation"); + const rect = saturationElement.getBoundingClientRect(); + const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); + const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height)); + const newS = x; + const newV = 1 - y; + const newHex = hsvToHex(displayHue, newS, newV); + onChange?.(newHex); + }; - const handleHueMouseDown = (e: React.MouseEvent) => { - e.preventDefault(); - setIsDragging("hue"); - const rect = hueRef.current!.getBoundingClientRect(); - const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); - const newH = x * 360; - setInternalHue(newH); - if (s > 0) { - const newHex = hsvToHex(newH, s, v); - onChange?.(newHex); - } - }; + const handleHueMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + const hueElement = hueRef.current; + if (!hueElement) return; + setIsDragging("hue"); + const rect = hueElement.getBoundingClientRect(); + const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); + const newH = x * 360; + setInternalHue(newH); + if (s > 0) { + const newHex = hsvToHex(newH, s, v); + onChange?.(newHex); + } + }; - const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { - const hex = e.target.value.replace("#", ""); - setInputValue(hex); - }; + const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const hex = e.target.value.replace("#", ""); + setInputValue(hex); + }; - const handleInputBlur = () => { - onChange?.(inputValue); - }; + const handleInputBlur = () => { + onChange?.(inputValue); + }; - const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === "Enter") { - onChange?.(inputValue); - e.currentTarget.blur(); - } - }; + const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === "Enter") { + onChange?.(inputValue); + e.currentTarget.blur(); + } + }; - const saturationStyle = { - background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${displayHue}, 100%, 50%))`, - }; + const saturationStyle = { + background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${displayHue}, 100%, 50%))`, + }; - const hueStyle = { - background: - "linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)", - }; + const hueStyle = { + background: + "linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)", + }; - return ( - <div className="relative"> - <div - ref={ref} - className={cn( - "bg-panel-accent h-9 rounded-full flex items-center gap-2 px-[0.45rem]", - className - )} - {...props} - > - <div - ref={triggerRef} - className="size-6 rounded-full cursor-pointer hover:ring-2 hover:ring-white/20 transition-all" - style={{ backgroundColor: `#${value}` }} - onClick={() => { - if (!isOpen && triggerRef.current && containerRef?.current) { - const containerRect = - containerRef.current.getBoundingClientRect(); - setPickerPosition({ - right: window.innerWidth - containerRect.left - 8, - bottom: window.innerHeight - containerRect.bottom, - }); - } - setIsOpen(!isOpen); - }} - /> - <div className="flex-1 flex items-center"> - <Input - className="bg-transparent p-0 !ring-0 !ring-offset-0 !border-0" - containerClassName="w-full" - value={inputValue} - onChange={handleInputChange} - onBlur={handleInputBlur} - onKeyDown={handleInputKeyDown} - /> - </div> - </div> + return ( + <div className="relative flex-1"> + <div + ref={ref} + className={cn( + "bg-panel-accent flex h-9 items-center gap-2 rounded-full px-[0.45rem]", + className, + )} + {...props} + > + <button + ref={triggerRef} + className="size-6 cursor-pointer rounded-full hover:ring-2 hover:ring-white/20" + style={{ backgroundColor: `#${value}` }} + type="button" + onClick={() => { + if (!isOpen && triggerRef.current && containerRef?.current) { + const containerRect = + containerRef.current.getBoundingClientRect(); + setPickerPosition({ + right: window.innerWidth - containerRect.left - 8, + bottom: window.innerHeight - containerRect.bottom, + }); + } + setIsOpen(!isOpen); + }} + /> + <div className="flex flex-1 items-center"> + <Input + className="!border-0 bg-transparent p-0 !ring-0 !ring-offset-0" + containerClassName="w-full" + value={inputValue} + onChange={handleInputChange} + onBlur={handleInputBlur} + onKeyDown={handleInputKeyDown} + /> + </div> + </div> - {isOpen && - createPortal( - <div - ref={pickerRef} - className="fixed z-50 p-4 bg-popover border border-border rounded-lg shadow-lg select-none" - style={{ - right: pickerPosition.right, - bottom: pickerPosition.bottom, - }} - > - <div - ref={saturationRef} - className="relative w-48 h-32 cursor-crosshair mb-3" - style={saturationStyle} - onMouseDown={handleSaturationMouseDown} - > - <ColorCircle - size="sm" - position={{ left: `${s * 100}%`, top: `${(1 - v) * 100}%` }} - color={`#${value}`} - /> - </div> + {isOpen && + createPortal( + <div + ref={pickerRef} + className="bg-popover border-border fixed z-50 rounded-lg border p-4 shadow-lg select-none" + style={{ + right: pickerPosition.right, + bottom: pickerPosition.bottom, + }} + > + <button + ref={saturationRef} + className="relative mb-3 h-32 w-48 cursor-crosshair appearance-none border-0 bg-transparent p-0" + style={saturationStyle} + type="button" + onMouseDown={handleSaturationMouseDown} + > + <ColorCircle + size="sm" + position={{ left: `${s * 100}%`, top: `${(1 - v) * 100}%` }} + color={`#${value}`} + /> + </button> - <div - ref={hueRef} - className="relative w-48 h-4 rounded-lg cursor-pointer" - style={hueStyle} - onMouseDown={handleHueMouseDown} - > - <ColorCircle - size="md" - position={{ - left: `${(displayHue / 360) * 100}%`, - top: "50%", - }} - color={`#${value}`} - /> - </div> - </div>, - document.body - )} - </div> - ); - } + <button + ref={hueRef} + className="relative h-4 w-48 cursor-pointer rounded-lg appearance-none border-0 bg-transparent p-0" + style={hueStyle} + type="button" + onMouseDown={handleHueMouseDown} + > + <ColorCircle + size="md" + position={{ + left: `${(displayHue / 360) * 100}%`, + top: "50%", + }} + color={`#${value}`} + /> + </button> + </div>, + document.body, + )} + </div> + ); + }, ); ColorPicker.displayName = "ColorPicker"; const ColorCircle = ({ - size, - position, - color, + size, + position, + color, }: { - size: "sm" | "md"; - position: { left: string; top: string }; - color: string; + size: "sm" | "md"; + position: { left: string; top: string }; + color: string; }) => ( - <div - className={`absolute border-3 border-white rounded-full shadow-lg pointer-events-none ${ - size === "sm" ? "w-3 h-3" : "w-4 h-4" - }`} - style={{ - left: position.left, - top: position.top, - transform: "translate(-50%, -50%)", - backgroundColor: color, - }} - /> + <div + className={`pointer-events-none absolute rounded-full border-3 border-white shadow-lg ${ + size === "sm" ? "size-3" : "size-4" + }`} + style={{ + left: position.left, + top: position.top, + transform: "translate(-50%, -50%)", + backgroundColor: color, + }} + /> ); export { ColorPicker }; diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx index 887c1ab7..0f77a4ce 100644 --- a/apps/web/src/components/ui/command.tsx +++ b/apps/web/src/components/ui/command.tsx @@ -1,153 +1,152 @@ "use client"; import * as React from "react"; -import { DialogProps } from "@radix-ui/react-dialog"; +import type { DialogProps } from "@radix-ui/react-dialog"; import { Command as CommandPrimitive } from "cmdk"; import { Search } from "lucide-react"; - -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; import { Dialog, DialogContent } from "./dialog"; const Command = React.forwardRef< - React.ElementRef<typeof CommandPrimitive>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive> + React.ElementRef<typeof CommandPrimitive>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive> >(({ className, ...props }, ref) => ( - <CommandPrimitive - ref={ref} - className={cn( - "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", - className - )} - {...props} - /> + <CommandPrimitive + ref={ref} + className={cn( + "bg-popover text-popover-foreground flex size-full flex-col overflow-hidden rounded-md", + className, + )} + {...props} + /> )); Command.displayName = CommandPrimitive.displayName; const CommandDialog = ({ children, ...props }: DialogProps) => { - return ( - <Dialog {...props}> - <DialogContent className="overflow-hidden p-0"> - <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> - {children} - </Command> - </DialogContent> - </Dialog> - ); + return ( + <Dialog {...props}> + <DialogContent className="overflow-hidden p-0"> + <Command className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:size-5"> + {children} + </Command> + </DialogContent> + </Dialog> + ); }; const CommandInput = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Input>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> + React.ElementRef<typeof CommandPrimitive.Input>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> >(({ className, ...props }, ref) => ( - <div className="flex items-center border-b px-3" cmdk-input-wrapper=""> - <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> - <CommandPrimitive.Input - ref={ref} - className={cn( - "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", - className - )} - {...props} - /> - </div> + <div className="flex items-center border-b px-3" cmdk-input-wrapper=""> + <Search className="mr-2 size-4 shrink-0 opacity-50" /> + <CommandPrimitive.Input + ref={ref} + className={cn( + "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50", + className, + )} + {...props} + /> + </div> )); CommandInput.displayName = CommandPrimitive.Input.displayName; const CommandList = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.List>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> + React.ElementRef<typeof CommandPrimitive.List>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> >(({ className, ...props }, ref) => ( - <CommandPrimitive.List - ref={ref} - className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} - {...props} - /> + <CommandPrimitive.List + ref={ref} + className={cn("max-h-[300px] overflow-x-hidden overflow-y-auto", className)} + {...props} + /> )); CommandList.displayName = CommandPrimitive.List.displayName; const CommandEmpty = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Empty>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> + React.ElementRef<typeof CommandPrimitive.Empty>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> >((props, ref) => ( - <CommandPrimitive.Empty - ref={ref} - className="py-6 text-center text-sm" - {...props} - /> + <CommandPrimitive.Empty + ref={ref} + className="py-6 text-center text-sm" + {...props} + /> )); CommandEmpty.displayName = CommandPrimitive.Empty.displayName; const CommandGroup = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Group>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> + React.ElementRef<typeof CommandPrimitive.Group>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> >(({ className, ...props }, ref) => ( - <CommandPrimitive.Group - ref={ref} - className={cn( - "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", - className - )} - {...props} - /> + <CommandPrimitive.Group + ref={ref} + className={cn( + "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium", + className, + )} + {...props} + /> )); CommandGroup.displayName = CommandPrimitive.Group.displayName; const CommandSeparator = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> + React.ElementRef<typeof CommandPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> >(({ className, ...props }, ref) => ( - <CommandPrimitive.Separator - ref={ref} - className={cn("-mx-1 h-px bg-border", className)} - {...props} - /> + <CommandPrimitive.Separator + ref={ref} + className={cn("bg-border -mx-1 h-px", className)} + {...props} + /> )); CommandSeparator.displayName = CommandPrimitive.Separator.displayName; const CommandItem = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> + React.ElementRef<typeof CommandPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> >(({ className, ...props }, ref) => ( - <CommandPrimitive.Item - ref={ref} - className={cn( - "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - className - )} - {...props} - /> + <CommandPrimitive.Item + ref={ref} + className={cn( + "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + className, + )} + {...props} + /> )); CommandItem.displayName = CommandPrimitive.Item.displayName; const CommandShortcut = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn( - "ml-auto text-xs tracking-widest text-muted-foreground", - className - )} - {...props} - /> - ); + return ( + <span + className={cn( + "text-muted-foreground ml-auto text-xs tracking-widest", + className, + )} + {...props} + /> + ); }; CommandShortcut.displayName = "CommandShortcut"; export { - Command, - CommandDialog, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandShortcut, - CommandSeparator, + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, }; diff --git a/apps/web/src/components/ui/context-menu.tsx b/apps/web/src/components/ui/context-menu.tsx index a1a28fa6..4af92764 100644 --- a/apps/web/src/components/ui/context-menu.tsx +++ b/apps/web/src/components/ui/context-menu.tsx @@ -2,10 +2,10 @@ import * as React from "react"; import { ContextMenu as ContextMenuPrimitive } from "radix-ui"; -import { Check, ChevronRight, Circle } from "lucide-react"; import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Tick02Icon, ArrowRightIcon, CircleIcon } from "@hugeicons/core-free-icons" const ContextMenu = ContextMenuPrimitive.Root; @@ -20,196 +20,215 @@ const ContextMenuSub = ContextMenuPrimitive.Sub; const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; const contextMenuItemVariants = cva( - "relative flex cursor-pointer select-none items-center gap-2 px-2 py-1.5 text-sm outline-hidden transition-opacity data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0", - { - variants: { - variant: { - default: "focus:opacity-65 focus:text-accent-foreground", - destructive: "text-destructive focus:text-destructive/80", - }, - }, - defaultVariants: { - variant: "default", - }, - } + "relative flex cursor-pointer select-none items-center gap-2.5 px-4 py-1.5 last:pb-1 text-base outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "focus:bg-accent/35 focus:text-accent-foreground [&_svg]:text-muted-foreground", + destructive: "text-destructive focus:bg-destructive/5 focus:text-destructive [&_svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + }, ); const ContextMenuSubTrigger = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & { - inset?: boolean; - variant?: VariantProps<typeof contextMenuItemVariants>["variant"]; - } ->(({ className, inset, children, variant = "default", ...props }, ref) => ( - <ContextMenuPrimitive.SubTrigger - ref={ref} - className={cn( - contextMenuItemVariants({ variant }), - "data-[state=open]:bg-accent data-[state=open]:opacity-65", - inset && "pl-8", - className - )} - {...props} - > - {children} - <ChevronRight className="ml-auto" /> - </ContextMenuPrimitive.SubTrigger> + React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & { + inset?: boolean; + variant?: VariantProps<typeof contextMenuItemVariants>["variant"]; + icon?: React.ReactNode; + } +>(({ className, inset, children, variant = "default", icon, ...props }, ref) => ( + <ContextMenuPrimitive.SubTrigger + ref={ref} + className={cn( + contextMenuItemVariants({ variant }), + "data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", + inset && "pl-8", + className, + )} + {...props} + > + {icon && <span className="size-4 shrink-0 text-muted-foreground">{icon}</span>} + {children} + <HugeiconsIcon icon={ArrowRightIcon} className="ml-auto text-muted-foreground/80" /> + </ContextMenuPrimitive.SubTrigger> )); ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; const ContextMenuSubContent = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.SubContent>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> + React.ElementRef<typeof ContextMenuPrimitive.SubContent>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent> >(({ className, ...props }, ref) => ( - <ContextMenuPrimitive.SubContent - ref={ref} - className={cn( - "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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", - className - )} - {...props} - /> + <ContextMenuPrimitive.SubContent + ref={ref} + className={cn( + "bg-popover text-popover-foreground z-50 min-w-48 overflow-hidden rounded-lg border shadow-xl py-2.5", + className, + )} + {...props} + /> )); ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; const ContextMenuContent = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> + React.ElementRef<typeof ContextMenuPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> >(({ className, ...props }, ref) => ( - <ContextMenuPrimitive.Portal> - <ContextMenuPrimitive.Content - ref={ref} - className={cn( - "z-50 min-w-32 overflow-hidden rounded-md border p-1 bg-popover text-popover-foreground shadow-md", - "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", - className - )} - {...props} - /> - </ContextMenuPrimitive.Portal> + <ContextMenuPrimitive.Portal> + <ContextMenuPrimitive.Content + ref={ref} + className={cn( + "bg-popover text-popover-foreground z-50 min-w-48 overflow-hidden rounded-lg border shadow-xl py-2.5", + className, + )} + {...props} + /> + </ContextMenuPrimitive.Portal> )); ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; const ContextMenuItem = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { - inset?: boolean; - variant?: VariantProps<typeof contextMenuItemVariants>["variant"]; - } ->(({ className, inset, variant = "default", ...props }, ref) => ( - <ContextMenuPrimitive.Item - ref={ref} - className={cn( - contextMenuItemVariants({ variant }), - inset && "pl-8", - className - )} - {...props} - /> + React.ElementRef<typeof ContextMenuPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { + inset?: boolean; + variant?: VariantProps<typeof contextMenuItemVariants>["variant"]; + icon?: React.ReactNode; + textRight?: string; + } +>(({ className, inset, variant = "default", icon, children, textRight, ...props }, ref) => ( + <ContextMenuPrimitive.Item + ref={ref} + className={cn( + contextMenuItemVariants({ variant }), + inset && "pl-8", + className, + )} + {...props} + > + {icon && <span className="[&_svg]:size-4 [&_svg]:shrink-0">{icon}</span>} + {children} + {textRight && ( + <span className="ml-auto text-[0.60rem] tracking-widest text-muted-foreground/80 mb-0.5"> + {textRight} + </span> + )} + </ContextMenuPrimitive.Item> )); ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; const ContextMenuCheckboxItem = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> & { - variant?: VariantProps<typeof contextMenuItemVariants>["variant"]; - } ->(({ className, children, checked, variant = "default", ...props }, ref) => ( - <ContextMenuPrimitive.CheckboxItem - ref={ref} - className={cn(contextMenuItemVariants({ variant }), "pl-8 pr-2", className)} - checked={checked} - {...props} - > - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <ContextMenuPrimitive.ItemIndicator> - <Check className="h-4 w-4" /> - </ContextMenuPrimitive.ItemIndicator> - </span> - {children} - </ContextMenuPrimitive.CheckboxItem> + React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem> & { + variant?: VariantProps<typeof contextMenuItemVariants>["variant"]; + icon?: React.ReactNode; + } +>(({ className, children, checked, variant = "default", icon, ...props }, ref) => ( + <ContextMenuPrimitive.CheckboxItem + ref={ref} + className={cn(contextMenuItemVariants({ variant }), "pr-2 pl-8", className)} + checked={checked} + {...props} + > + <span className="absolute left-2 flex size-3.5 items-center justify-center"> + <ContextMenuPrimitive.ItemIndicator> + <HugeiconsIcon icon={Tick02Icon} className="size-4" /> + </ContextMenuPrimitive.ItemIndicator> + </span> + {icon && <span className="size-4 shrink-0 text-muted-foreground">{icon}</span>} + {children} + </ContextMenuPrimitive.CheckboxItem> )); ContextMenuCheckboxItem.displayName = - ContextMenuPrimitive.CheckboxItem.displayName; + ContextMenuPrimitive.CheckboxItem.displayName; const ContextMenuRadioItem = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.RadioItem>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> & { - variant?: VariantProps<typeof contextMenuItemVariants>["variant"]; - } ->(({ className, children, variant = "default", ...props }, ref) => ( - <ContextMenuPrimitive.RadioItem - ref={ref} - className={cn(contextMenuItemVariants({ variant }), "pl-8 pr-2", className)} - {...props} - > - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <ContextMenuPrimitive.ItemIndicator> - <Circle className="h-2 w-2 fill-current" /> - </ContextMenuPrimitive.ItemIndicator> - </span> - {children} - </ContextMenuPrimitive.RadioItem> + React.ElementRef<typeof ContextMenuPrimitive.RadioItem>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem> & { + variant?: VariantProps<typeof contextMenuItemVariants>["variant"]; + icon?: React.ReactNode; + } +>(({ className, children, variant = "default", icon, ...props }, ref) => ( + <ContextMenuPrimitive.RadioItem + ref={ref} + className={cn(contextMenuItemVariants({ variant }), "pr-2 pl-8", className)} + {...props} + > + <span className="absolute left-2 flex size-3.5 items-center justify-center"> + <ContextMenuPrimitive.ItemIndicator> + <HugeiconsIcon icon={CircleIcon} className="size-2 fill-current" /> + </ContextMenuPrimitive.ItemIndicator> + </span> + {icon && <span className="size-4 shrink-0 text-muted-foreground">{icon}</span>} + {children} + </ContextMenuPrimitive.RadioItem> )); ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; const ContextMenuLabel = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.Label>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - <ContextMenuPrimitive.Label - ref={ref} - className={cn( - "px-2 py-1.5 text-sm font-semibold", - inset && "pl-8", - className - )} - {...props} - /> + React.ElementRef<typeof ContextMenuPrimitive.Label>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & { + inset?: boolean; + icon?: React.ReactNode; + } +>(({ className, inset, icon, children, ...props }, ref) => ( + <ContextMenuPrimitive.Label + ref={ref} + className={cn( + "flex items-center gap-2.5 px-4 py-1.5 text-sm font-semibold text-foreground", + inset && "pl-8", + className, + )} + {...props} + > + {icon && <span className="size-4 shrink-0 text-muted-foreground">{icon}</span>} + {children} + </ContextMenuPrimitive.Label> )); ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; const ContextMenuSeparator = React.forwardRef< - React.ElementRef<typeof ContextMenuPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator> + React.ElementRef<typeof ContextMenuPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator> >(({ className, ...props }, ref) => ( - <ContextMenuPrimitive.Separator - ref={ref} - className={cn("-mx-1 my-1 h-px bg-foreground/10", className)} - {...props} - /> + <ContextMenuPrimitive.Separator + ref={ref} + className={cn("bg-border my-2 h-px", className)} + {...props} + /> )); ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; const ContextMenuShortcut = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn("ml-auto text-xs tracking-widest opacity-60", className)} - {...props} - /> - ); + return ( + <span + className={cn("ml-auto text-xs tracking-widest text-muted-foreground opacity-60", className)} + {...props} + /> + ); }; ContextMenuShortcut.displayName = "ContextMenuShortcut"; export { - ContextMenu, - ContextMenuTrigger, - ContextMenuContent, - ContextMenuItem, - ContextMenuCheckboxItem, - ContextMenuRadioItem, - ContextMenuLabel, - ContextMenuSeparator, - ContextMenuShortcut, - ContextMenuGroup, - ContextMenuPortal, - ContextMenuSub, - ContextMenuSubContent, - ContextMenuSubTrigger, - ContextMenuRadioGroup, + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, }; diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx index 42d0f80d..3577f796 100644 --- a/apps/web/src/components/ui/dialog.tsx +++ b/apps/web/src/components/ui/dialog.tsx @@ -3,9 +3,7 @@ import * as React from "react"; import { Dialog as DialogPrimitive } from "radix-ui"; import { X } from "lucide-react"; - -import { cn } from "../../lib/utils"; -import { ScrollArea } from "./scroll-area"; +import { cn } from "@/utils/ui"; const Dialog = DialogPrimitive.Root; @@ -16,112 +14,118 @@ const DialogPortal = DialogPrimitive.Portal; const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> + React.ElementRef<typeof DialogPrimitive.Overlay>, + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> >(({ className, ...props }, ref) => ( - <DialogPrimitive.Overlay - ref={ref} - className={cn( - "fixed inset-0 z-250 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", - className - )} - {...props} - /> + <DialogPrimitive.Overlay + ref={ref} + className={cn( + "fixed inset-0 z-250 backdrop-blur-sm bg-black/10", + className, + )} + {...props} + /> )); DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> + React.ElementRef<typeof DialogPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> >(({ className, children, ...props }, ref) => ( - <DialogPortal> - <DialogOverlay /> - <DialogPrimitive.Content - ref={ref} - className={cn( - "fixed left-[50%] top-[50%] p-6 z-250 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-popover shadow-lg duration-200 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 rounded-lg", - className - )} - onCloseAutoFocus={(e) => { - e.stopPropagation(); - e.preventDefault(); - }} - {...props} - > - {children} - <DialogPrimitive.Close className="absolute right-4 top-4 opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground cursor-pointer"> - <X className="h-4 w-4" /> - <span className="sr-only">Close</span> - </DialogPrimitive.Close> - </DialogPrimitive.Content> - </DialogPortal> + <DialogPortal> + <DialogOverlay /> + <DialogPrimitive.Content + ref={ref} + className={cn( + "bg-popover fixed top-[50%] left-[50%] z-250 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200", + className, + )} + onCloseAutoFocus={(e) => { + e.stopPropagation(); + e.preventDefault(); + }} + {...props} + > + {children} + <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-6 right-6 cursor-pointer opacity-70 hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> + <X className="size-5 text-muted-foreground" /> + <span className="sr-only">Close</span> + </DialogPrimitive.Close> + </DialogPrimitive.Content> + </DialogPortal> )); DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col space-y-2 text-center sm:text-left", - className - )} - {...props} - /> + <div + className={cn("flex flex-col space-y-2 text-left border-b p-6", className)} + {...props} + /> ); DialogHeader.displayName = "DialogHeader"; +const DialogBody = ({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) => ( + <div className={cn("p-6 flex flex-col gap-6", className)} {...props} /> +); +DialogBody.displayName = "DialogBody"; + const DialogFooter = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", - className - )} - {...props} - /> + <div + className={cn( + "flex gap-3 flex-col-reverse sm:flex-row sm:justify-end p-6 py-5 border-t", + className, + )} + {...props} + /> ); DialogFooter.displayName = "DialogFooter"; const DialogTitle = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> + React.ElementRef<typeof DialogPrimitive.Title>, + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> >(({ className, ...props }, ref) => ( - <DialogPrimitive.Title - ref={ref} - className={cn( - "text-lg font-semibold leading-none tracking-tight", - className - )} - {...props} - /> + <DialogPrimitive.Title + ref={ref} + className={cn( + "text-lg leading-none font-semibold tracking-tight", + className, + )} + {...props} + /> )); DialogTitle.displayName = DialogPrimitive.Title.displayName; const DialogDescription = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> + React.ElementRef<typeof DialogPrimitive.Description>, + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> >(({ className, ...props }, ref) => ( - <DialogPrimitive.Description - ref={ref} - className={cn("text-sm text-muted-foreground", className)} - {...props} - /> + <DialogPrimitive.Description + ref={ref} + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> )); DialogDescription.displayName = DialogPrimitive.Description.displayName; export { - Dialog, - DialogPortal, - DialogOverlay, - DialogTrigger, - DialogClose, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogBody, + DialogFooter, + DialogTitle, + DialogDescription, }; diff --git a/apps/web/src/components/ui/draggable-item.tsx b/apps/web/src/components/ui/draggable-item.tsx deleted file mode 100644 index 9f5c7887..00000000 --- a/apps/web/src/components/ui/draggable-item.tsx +++ /dev/null @@ -1,247 +0,0 @@ -"use client"; - -import { AspectRatio } from "@/components/ui/aspect-ratio"; -import { Button } from "@/components/ui/button"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { ReactNode, useState, useRef, useEffect } from "react"; -import { createPortal } from "react-dom"; -import { Plus } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { usePlaybackStore } from "@/stores/playback-store"; -import { DragData } from "@/types/timeline"; - -export interface DraggableMediaItemProps { - name: string; - preview: ReactNode; - dragData: DragData; - onDragStart?: (e: React.DragEvent) => void; - onAddToTimeline?: (currentTime: number) => void; - aspectRatio?: number; - className?: string; - containerClassName?: string; - showPlusOnDrag?: boolean; - showLabel?: boolean; - rounded?: boolean; - variant?: "card" | "compact"; - isDraggable?: boolean; - isHighlighted?: boolean; -} - -export function DraggableMediaItem({ - name, - preview, - dragData, - onDragStart, - onAddToTimeline, - aspectRatio = 16 / 9, - className = "", - containerClassName, - showPlusOnDrag = true, - showLabel = true, - rounded = true, - variant = "card", - isDraggable = true, - isHighlighted = false, -}: DraggableMediaItemProps) { - const [isDragging, setIsDragging] = useState(false); - const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 }); - const dragRef = useRef<HTMLDivElement>(null); - const currentTime = isDraggable - ? usePlaybackStore((state) => state.currentTime) - : 0; - const highlightClassName = "ring-2 ring-primary rounded-sm bg-primary/10"; - - const handleAddToTimeline = () => { - onAddToTimeline?.(currentTime); - }; - - const emptyImg = new window.Image(); - emptyImg.src = - "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; - - useEffect(() => { - if (!isDragging) return; - - const handleDragOver = (e: DragEvent) => { - setDragPosition({ x: e.clientX, y: e.clientY }); - }; - - document.addEventListener("dragover", handleDragOver); - - return () => { - document.removeEventListener("dragover", handleDragOver); - }; - }, [isDragging]); - - const handleDragStart = (e: React.DragEvent) => { - e.dataTransfer.setDragImage(emptyImg, 0, 0); - - // Set drag data - e.dataTransfer.setData( - "application/x-media-item", - JSON.stringify(dragData) - ); - e.dataTransfer.effectAllowed = "copy"; - - // Set initial position and show custom drag preview - setDragPosition({ x: e.clientX, y: e.clientY }); - setIsDragging(true); - - onDragStart?.(e); - }; - - const handleDragEnd = () => { - setIsDragging(false); - }; - - return ( - <> - {variant === "card" ? ( - <div - ref={dragRef} - className={cn("relative group", containerClassName ?? "w-28 h-28")} - > - <div - className={cn( - "flex flex-col gap-1 p-1 h-auto w-full relative cursor-default", - className, - isHighlighted && highlightClassName - )} - > - <AspectRatio - ratio={aspectRatio} - className={cn( - "bg-panel-accent relative overflow-hidden", - rounded && "rounded-md", - isDraggable && "[&::-webkit-drag-ghost]:opacity-0" // Webkit-specific ghost hiding - )} - draggable={isDraggable} - onDragStart={isDraggable ? handleDragStart : undefined} - onDragEnd={isDraggable ? handleDragEnd : undefined} - > - {preview} - {!isDragging && ( - <PlusButton - className="opacity-0 group-hover:opacity-100" - onClick={handleAddToTimeline} - /> - )} - </AspectRatio> - {showLabel && ( - <span - className="text-[0.7rem] text-muted-foreground truncate w-full text-left" - aria-label={name} - title={name} - > - {name.length > 8 - ? `${name.slice(0, 16)}...${name.slice(-3)}` - : name} - </span> - )} - </div> - </div> - ) : ( - <div - ref={dragRef} - className={cn( - "relative group w-full", - isHighlighted && highlightClassName - )} - > - <div - className={cn( - "h-8 flex items-center gap-3 cursor-default w-full px-1", - isDraggable && "[&::-webkit-drag-ghost]:opacity-0", - className - )} - draggable={isDraggable} - onDragStart={isDraggable ? handleDragStart : undefined} - onDragEnd={isDraggable ? handleDragEnd : undefined} - > - <div className="w-6 h-6 flex-shrink-0 rounded-[0.35rem] overflow-hidden"> - {preview} - </div> - <span className="text-sm truncate flex-1 w-full">{name}</span> - </div> - </div> - )} - - {/* Custom drag preview */} - {isDraggable && - isDragging && - typeof document !== "undefined" && - createPortal( - <div - className="fixed pointer-events-none z-9999" - style={{ - left: dragPosition.x - 40, // Center the preview (half of 80px) - top: dragPosition.y - 40, // Center the preview (half of 80px) - }} - > - <div className="w-[80px]"> - <AspectRatio - ratio={1} - className="relative rounded-md overflow-hidden shadow-2xl ring-3 ring-primary" - > - <div className="w-full h-full [&_img]:w-full [&_img]:h-full [&_img]:object-cover [&_img]:rounded-none"> - {preview} - </div> - {showPlusOnDrag && ( - <PlusButton - onClick={handleAddToTimeline} - tooltipText="Add to timeline or drag to position" - /> - )} - </AspectRatio> - </div> - </div>, - document.body - )} - </> - ); -} - -function PlusButton({ - className, - onClick, - tooltipText, -}: { - className?: string; - onClick?: () => void; - tooltipText?: string; -}) { - const button = ( - <Button - size="icon" - className={cn( - "absolute bottom-2 right-2 size-5 bg-background hover:bg-panel text-foreground", - className - )} - onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - onClick?.(); - }} - title={tooltipText} - > - <Plus className="size-4!" /> - </Button> - ); - - if (tooltipText) { - return ( - <Tooltip> - <TooltipTrigger asChild>{button}</TooltipTrigger> - <TooltipContent> - <p>{tooltipText}</p> - </TooltipContent> - </Tooltip> - ); - } - - return button; -} diff --git a/apps/web/src/components/ui/drawer.tsx b/apps/web/src/components/ui/drawer.tsx index 7e065e5c..d5752d62 100644 --- a/apps/web/src/components/ui/drawer.tsx +++ b/apps/web/src/components/ui/drawer.tsx @@ -3,16 +3,16 @@ import * as React from "react"; import { Drawer as DrawerPrimitive } from "vaul"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Drawer = ({ - shouldScaleBackground = true, - ...props + shouldScaleBackground = true, + ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) => ( - <DrawerPrimitive.Root - shouldScaleBackground={shouldScaleBackground} - {...props} - /> + <DrawerPrimitive.Root + shouldScaleBackground={shouldScaleBackground} + {...props} + /> ); Drawer.displayName = "Drawer"; @@ -23,96 +23,96 @@ const DrawerPortal = DrawerPrimitive.Portal; const DrawerClose = DrawerPrimitive.Close; const DrawerOverlay = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> + React.ElementRef<typeof DrawerPrimitive.Overlay>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> >(({ className, ...props }, ref) => ( - <DrawerPrimitive.Overlay - ref={ref} - className={cn("fixed inset-0 z-50 bg-black/80", className)} - {...props} - /> + <DrawerPrimitive.Overlay + ref={ref} + className={cn("fixed inset-0 z-50 bg-black/80", className)} + {...props} + /> )); DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; const DrawerContent = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> + React.ElementRef<typeof DrawerPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> >(({ className, children, ...props }, ref) => ( - <DrawerPortal> - <DrawerOverlay /> - <DrawerPrimitive.Content - ref={ref} - className={cn( - "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background", - className - )} - {...props} - > - <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" /> - {children} - </DrawerPrimitive.Content> - </DrawerPortal> + <DrawerPortal> + <DrawerOverlay /> + <DrawerPrimitive.Content + ref={ref} + className={cn( + "bg-background fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border", + className, + )} + {...props} + > + <div className="bg-muted mx-auto mt-4 h-2 w-[100px] rounded-full" /> + {children} + </DrawerPrimitive.Content> + </DrawerPortal> )); DrawerContent.displayName = "DrawerContent"; const DrawerHeader = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} - {...props} - /> + <div + className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} + {...props} + /> ); DrawerHeader.displayName = "DrawerHeader"; const DrawerFooter = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn("mt-auto flex flex-col gap-2 p-4", className)} - {...props} - /> + <div + className={cn("mt-auto flex flex-col gap-2 p-4", className)} + {...props} + /> ); DrawerFooter.displayName = "DrawerFooter"; const DrawerTitle = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> + React.ElementRef<typeof DrawerPrimitive.Title>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> >(({ className, ...props }, ref) => ( - <DrawerPrimitive.Title - ref={ref} - className={cn( - "text-lg font-semibold leading-none tracking-tight", - className - )} - {...props} - /> + <DrawerPrimitive.Title + ref={ref} + className={cn( + "text-lg leading-none font-semibold tracking-tight", + className, + )} + {...props} + /> )); DrawerTitle.displayName = DrawerPrimitive.Title.displayName; const DrawerDescription = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> + React.ElementRef<typeof DrawerPrimitive.Description>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> >(({ className, ...props }, ref) => ( - <DrawerPrimitive.Description - ref={ref} - className={cn("text-sm text-muted-foreground", className)} - {...props} - /> + <DrawerPrimitive.Description + ref={ref} + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> )); DrawerDescription.displayName = DrawerPrimitive.Description.displayName; export { - Drawer, - DrawerPortal, - DrawerOverlay, - DrawerTrigger, - DrawerClose, - DrawerContent, - DrawerHeader, - DrawerFooter, - DrawerTitle, - DrawerDescription, + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, }; diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx index 823d48a3..a7736e6e 100644 --- a/apps/web/src/components/ui/dropdown-menu.tsx +++ b/apps/web/src/components/ui/dropdown-menu.tsx @@ -5,7 +5,7 @@ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; import { Check, ChevronRight, Circle } from "lucide-react"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const DropdownMenu = DropdownMenuPrimitive.Root; @@ -20,215 +20,215 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub; const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; const dropdownMenuItemVariants = cva( - "relative flex cursor-pointer select-none items-center gap-2 px-2 py-1.5 text-sm outline-hidden transition-opacity data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0", - { - variants: { - variant: { - default: "focus:opacity-65 focus:text-accent-foreground", - destructive: "text-destructive focus:text-destructive/80", - }, - }, - defaultVariants: { - variant: "default", - }, - } + "relative flex cursor-pointer select-none items-center gap-2 rounded-lg px-2.5 py-2 text-sm text-foreground/85 outline-hidden data-[highlighted]:bg-accent/35 data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0", + { + variants: { + variant: { + default: "", + destructive: + "text-destructive data-[highlighted]:bg-destructive/5 data-[highlighted]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + }, ); const DropdownMenuSubTrigger = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { - inset?: boolean; - variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"]; - } + React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { + inset?: boolean; + variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"]; + } >(({ className, inset, children, variant = "default", ...props }, ref) => ( - <DropdownMenuPrimitive.SubTrigger - ref={ref} - className={cn( - dropdownMenuItemVariants({ variant }), - "data-[state=open]:bg-accent data-[state=open]:opacity-65", - inset && "pl-8", - className - )} - {...props} - > - {children} - <ChevronRight className="ml-auto" /> - </DropdownMenuPrimitive.SubTrigger> + <DropdownMenuPrimitive.SubTrigger + ref={ref} + className={cn( + dropdownMenuItemVariants({ variant }), + "data-[state=open]:bg-muted data-[state=open]:text-foreground", + inset && "pl-8", + className, + )} + {...props} + > + {children} + <ChevronRight className="ml-auto" /> + </DropdownMenuPrimitive.SubTrigger> )); DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName; + DropdownMenuPrimitive.SubTrigger.displayName; const DropdownMenuSubContent = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> + React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> >(({ className, ...props }, ref) => ( - <DropdownMenuPrimitive.SubContent - ref={ref} - className={cn( - "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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", - className - )} - {...props} - /> + <DropdownMenuPrimitive.SubContent + ref={ref} + className={cn( + "bg-popover text-popover-foreground z-50 min-w-32 overflow-hidden rounded-2xl border p-2 shadow-lg", + className, + )} + {...props} + /> )); DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName; + DropdownMenuPrimitive.SubContent.displayName; const DropdownMenuContent = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> + React.ElementRef<typeof DropdownMenuPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> >(({ className, sideOffset = 4, ...props }, ref) => ( - <DropdownMenuPrimitive.Portal> - <DropdownMenuPrimitive.Content - ref={ref} - sideOffset={sideOffset} - onCloseAutoFocus={(e) => { - e.stopPropagation(); - e.preventDefault(); - }} - className={cn( - "z-50 min-w-32 overflow-hidden rounded-md border p-1 bg-popover text-popover-foreground shadow-md", - "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", - className - )} - {...props} - /> - </DropdownMenuPrimitive.Portal> + <DropdownMenuPrimitive.Portal> + <DropdownMenuPrimitive.Content + ref={ref} + sideOffset={sideOffset} + onCloseAutoFocus={(e) => { + e.stopPropagation(); + e.preventDefault(); + }} + className={cn( + "bg-popover text-popover-foreground z-50 min-w-32 overflow-hidden rounded-lg border p-2 shadow-lg", + className, + )} + {...props} + /> + </DropdownMenuPrimitive.Portal> )); DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; const DropdownMenuItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { - inset?: boolean; - variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"]; - } + React.ElementRef<typeof DropdownMenuPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { + inset?: boolean; + variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"]; + } >(({ className, inset, variant = "default", ...props }, ref) => ( - <DropdownMenuPrimitive.Item - ref={ref} - className={cn( - dropdownMenuItemVariants({ variant }), - inset && "pl-8", - className - )} - {...props} - /> + <DropdownMenuPrimitive.Item + ref={ref} + className={cn( + dropdownMenuItemVariants({ variant }), + inset && "pl-8", + className, + )} + {...props} + /> )); DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> & { - variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"]; - } + React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> & { + variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"]; + } >(({ className, children, checked, variant = "default", ...props }, ref) => ( - <DropdownMenuPrimitive.CheckboxItem - ref={ref} - className={cn( - dropdownMenuItemVariants({ variant }), - "pl-2 pr-8", - className - )} - checked={checked} - onSelect={(e) => { - e.preventDefault(); - }} - {...props} - > - {children} - <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Check className="h-4 w-4" /> - </DropdownMenuPrimitive.ItemIndicator> - </span> - </DropdownMenuPrimitive.CheckboxItem> + <DropdownMenuPrimitive.CheckboxItem + ref={ref} + className={cn( + dropdownMenuItemVariants({ variant }), + "pr-8 pl-2", + className, + )} + checked={checked} + onSelect={(e) => { + e.preventDefault(); + }} + {...props} + > + {children} + <span className="absolute right-2 flex size-3.5 items-center justify-center"> + <DropdownMenuPrimitive.ItemIndicator> + <Check className="size-4" /> + </DropdownMenuPrimitive.ItemIndicator> + </span> + </DropdownMenuPrimitive.CheckboxItem> )); DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName; + DropdownMenuPrimitive.CheckboxItem.displayName; const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> & { - variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"]; - } + React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> & { + variant?: VariantProps<typeof dropdownMenuItemVariants>["variant"]; + } >(({ className, children, variant = "default", ...props }, ref) => ( - <DropdownMenuPrimitive.RadioItem - ref={ref} - className={cn( - dropdownMenuItemVariants({ variant }), - "pl-8 pr-2", - className - )} - {...props} - > - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Circle className="h-2 w-2 fill-current" /> - </DropdownMenuPrimitive.ItemIndicator> - </span> - {children} - </DropdownMenuPrimitive.RadioItem> + <DropdownMenuPrimitive.RadioItem + ref={ref} + className={cn( + dropdownMenuItemVariants({ variant }), + "pr-2 pl-8", + className, + )} + {...props} + > + <span className="absolute left-2 flex size-3.5 items-center justify-center"> + <DropdownMenuPrimitive.ItemIndicator> + <Circle className="size-2 fill-current" /> + </DropdownMenuPrimitive.ItemIndicator> + </span> + {children} + </DropdownMenuPrimitive.RadioItem> )); DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; const DropdownMenuLabel = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Label>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { - inset?: boolean; - } + React.ElementRef<typeof DropdownMenuPrimitive.Label>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { + inset?: boolean; + } >(({ className, inset, ...props }, ref) => ( - <DropdownMenuPrimitive.Label - ref={ref} - className={cn( - "px-2 py-1.5 text-sm font-semibold", - inset && "pl-8", - className - )} - {...props} - /> + <DropdownMenuPrimitive.Label + ref={ref} + className={cn( + "px-3 pb-2 pt-1 text-[11px] font-bold uppercase tracking-wider text-muted-foreground", + inset && "pl-8", + className, + )} + {...props} + /> )); DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; const DropdownMenuSeparator = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> + React.ElementRef<typeof DropdownMenuPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> >(({ className, ...props }, ref) => ( - <DropdownMenuPrimitive.Separator - ref={ref} - className={cn("-mx-1 my-1 h-px bg-foreground/10", className)} - {...props} - /> + <DropdownMenuPrimitive.Separator + ref={ref} + className={cn("bg-border/60 mx-1 my-2 h-px", className)} + {...props} + /> )); DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; const DropdownMenuShortcut = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn("ml-auto text-xs tracking-widest opacity-60", className)} - {...props} - /> - ); + return ( + <span + className={cn("ml-auto text-xs tracking-widest opacity-60", className)} + {...props} + /> + ); }; DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; export { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuRadioGroup, + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, }; diff --git a/apps/web/src/components/ui/editable-timecode.tsx b/apps/web/src/components/ui/editable-timecode.tsx deleted file mode 100644 index 404831f6..00000000 --- a/apps/web/src/components/ui/editable-timecode.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client";
-
-import { useState, useRef, useEffect } from "react";
-import { cn } from "@/lib/utils";
-import { formatTimeCode, parseTimeCode, TimeCode } from "@/lib/time";
-import { DEFAULT_FPS } from "@/stores/project-store";
-
-interface EditableTimecodeProps {
- time: number;
- duration?: number;
- format?: TimeCode;
- fps?: number;
- onTimeChange?: (time: number) => void;
- className?: string;
- disabled?: boolean;
-}
-
-export function EditableTimecode({
- time,
- duration,
- format = "HH:MM:SS:FF",
- fps = DEFAULT_FPS,
- onTimeChange,
- className,
- disabled = false,
-}: EditableTimecodeProps) {
- const [isEditing, setIsEditing] = useState(false);
- const [inputValue, setInputValue] = useState("");
- const [hasError, setHasError] = useState(false);
- const inputRef = useRef<HTMLInputElement>(null);
- const enterPressedRef = useRef(false);
-
- const formattedTime = formatTimeCode(time, format, fps);
-
- const startEditing = () => {
- if (disabled) return;
- setIsEditing(true);
- setInputValue(formattedTime);
- setHasError(false);
- enterPressedRef.current = false;
- };
-
- const cancelEditing = () => {
- setIsEditing(false);
- setInputValue("");
- setHasError(false);
- enterPressedRef.current = false;
- };
-
- const applyEdit = () => {
- const parsedTime = parseTimeCode(inputValue, format, fps);
-
- if (parsedTime === null) {
- setHasError(true);
- return;
- }
-
- // Clamp time to valid range
- const clampedTime = Math.max(
- 0,
- duration ? Math.min(duration, parsedTime) : parsedTime
- );
-
- onTimeChange?.(clampedTime);
- setIsEditing(false);
- setInputValue("");
- setHasError(false);
- enterPressedRef.current = false;
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Enter") {
- e.preventDefault();
- enterPressedRef.current = true;
- applyEdit();
- } else if (e.key === "Escape") {
- e.preventDefault();
- cancelEditing();
- }
- };
-
- const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- setInputValue(e.target.value);
- setHasError(false);
- };
-
- const handleBlur = () => {
- // Only apply edit if Enter wasn't pressed (to avoid double processing)
- if (!enterPressedRef.current && isEditing) {
- applyEdit();
- }
- };
-
- // Focus input when entering edit mode
- useEffect(() => {
- if (isEditing && inputRef.current) {
- inputRef.current.focus();
- inputRef.current.select();
- }
- }, [isEditing]);
-
- if (isEditing) {
- return (
- <input
- ref={inputRef}
- type="text"
- value={inputValue}
- onChange={handleInputChange}
- onKeyDown={handleKeyDown}
- onBlur={handleBlur}
- className={cn(
- "text-xs font-mono bg-transparent border-none outline-none",
- "focus:bg-background focus:border focus:border-primary focus:px-1 focus:rounded",
- "tabular-nums text-primary",
- hasError && "text-destructive focus:border-destructive",
- className
- )}
- style={{ width: `${formattedTime.length + 1}ch` }}
- placeholder={formattedTime}
- />
- );
- }
-
- return (
- <span
- onClick={startEditing}
- className={cn(
- "text-xs font-mono tabular-nums text-primary cursor-pointer",
- "hover:bg-muted/50 hover:rounded px-1 -mx-1 transition-colors",
- disabled && "cursor-default hover:bg-transparent",
- className
- )}
- title={disabled ? undefined : "Click to edit time"}
- >
- {formattedTime}
- </span>
- );
-}
diff --git a/apps/web/src/components/ui/font-picker.tsx b/apps/web/src/components/ui/font-picker.tsx index 7bb11274..1806b48a 100644 --- a/apps/web/src/components/ui/font-picker.tsx +++ b/apps/web/src/components/ui/font-picker.tsx @@ -1,40 +1,42 @@ import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; -import { FONT_OPTIONS, FontFamily } from "@/constants/font-constants"; +import { FONT_OPTIONS, type FontFamily } from "@/constants/font-constants"; interface FontPickerProps { - defaultValue?: FontFamily; - onValueChange?: (value: FontFamily) => void; - className?: string; + defaultValue?: FontFamily; + onValueChange?: (value: FontFamily) => void; + className?: string; } export function FontPicker({ - defaultValue, - onValueChange, - className, + defaultValue, + onValueChange, + className, }: FontPickerProps) { - return ( - <Select defaultValue={defaultValue} onValueChange={onValueChange}> - <SelectTrigger className={`w-full bg-panel-accent h-8 text-xs ${className || ""}`}> - <SelectValue placeholder="Select a font" /> - </SelectTrigger> - <SelectContent> - {FONT_OPTIONS.map((font) => ( - <SelectItem - key={font.value} - value={font.value} - className="text-xs" - style={{ fontFamily: font.value }} - > - {font.label} - </SelectItem> - ))} - </SelectContent> - </Select> - ); + return ( + <Select defaultValue={defaultValue} onValueChange={onValueChange}> + <SelectTrigger + className={`bg-panel-accent h-9 w-full text-base ${className || ""}`} + > + <SelectValue placeholder="Select a font" className="text-sm" /> + </SelectTrigger> + <SelectContent> + {FONT_OPTIONS.map((font) => ( + <SelectItem + key={font.value} + value={font.value} + className="text-sn" + style={{ fontFamily: font.value }} + > + {font.label} + </SelectItem> + ))} + </SelectContent> + </Select> + ); } diff --git a/apps/web/src/components/ui/form.tsx b/apps/web/src/components/ui/form.tsx index cab151e1..a6a18752 100644 --- a/apps/web/src/components/ui/form.tsx +++ b/apps/web/src/components/ui/form.tsx @@ -1,177 +1,177 @@ "use client"; import * as React from "react"; -import { Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui"; +import { type Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui"; import { - Controller, - ControllerProps, - FieldPath, - FieldValues, - FormProvider, - useFormContext, + Controller, + type ControllerProps, + type FieldPath, + type FieldValues, + FormProvider, + useFormContext, } from "react-hook-form"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; import { Label } from "./label"; const Form = FormProvider; type FormFieldContextValue< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, > = { - name: TName; + name: TName; }; const FormFieldContext = React.createContext<FormFieldContextValue>( - {} as FormFieldContextValue + {} as FormFieldContextValue, ); const FormField = < - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, >({ - ...props + ...props }: ControllerProps<TFieldValues, TName>) => { - return ( - <FormFieldContext.Provider value={{ name: props.name }}> - <Controller {...props} /> - </FormFieldContext.Provider> - ); + return ( + <FormFieldContext.Provider value={{ name: props.name }}> + <Controller {...props} /> + </FormFieldContext.Provider> + ); }; const useFormField = () => { - const fieldContext = React.useContext(FormFieldContext); - const itemContext = React.useContext(FormItemContext); - const { getFieldState, formState } = useFormContext(); + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); - const fieldState = getFieldState(fieldContext.name, formState); + const fieldState = getFieldState(fieldContext.name, formState); - if (!fieldContext) { - throw new Error("useFormField should be used within <FormField>"); - } + if (!fieldContext) { + throw new Error("useFormField should be used within <FormField>"); + } - const { id } = itemContext; + const { id } = itemContext; - return { - id, - name: fieldContext.name, - formItemId: `${id}-form-item`, - formDescriptionId: `${id}-form-item-description`, - formMessageId: `${id}-form-item-message`, - ...fieldState, - }; + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; }; type FormItemContextValue = { - id: string; + id: string; }; const FormItemContext = React.createContext<FormItemContextValue>( - {} as FormItemContextValue + {} as FormItemContextValue, ); const FormItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, ...props }, ref) => { - const id = React.useId(); + const id = React.useId(); - return ( - <FormItemContext.Provider value={{ id }}> - <div ref={ref} className={cn("space-y-2", className)} {...props} /> - </FormItemContext.Provider> - ); + return ( + <FormItemContext.Provider value={{ id }}> + <div ref={ref} className={cn("space-y-2", className)} {...props} /> + </FormItemContext.Provider> + ); }); FormItem.displayName = "FormItem"; const FormLabel = React.forwardRef< - React.ElementRef<typeof LabelPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> + React.ElementRef<typeof LabelPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> >(({ className, ...props }, ref) => { - const { error, formItemId } = useFormField(); + const { error, formItemId } = useFormField(); - return ( - <Label - ref={ref} - className={cn(error && "text-destructive", className)} - htmlFor={formItemId} - {...props} - /> - ); + return ( + <Label + ref={ref} + className={cn(error && "text-destructive", className)} + htmlFor={formItemId} + {...props} + /> + ); }); FormLabel.displayName = "FormLabel"; const FormControl = React.forwardRef< - React.ElementRef<typeof SlotPrimitive.Slot>, - React.ComponentPropsWithoutRef<typeof SlotPrimitive.Slot> + React.ElementRef<typeof SlotPrimitive.Slot>, + React.ComponentPropsWithoutRef<typeof SlotPrimitive.Slot> >(({ ...props }, ref) => { - const { error, formItemId, formDescriptionId, formMessageId } = - useFormField(); + const { error, formItemId, formDescriptionId, formMessageId } = + useFormField(); - return ( - <SlotPrimitive.Slot - ref={ref} - id={formItemId} - aria-describedby={ - error ? `${formDescriptionId} ${formMessageId}` : `${formDescriptionId}` - } - aria-invalid={!!error} - {...props} - /> - ); + return ( + <SlotPrimitive.Slot + ref={ref} + id={formItemId} + aria-describedby={ + error ? `${formDescriptionId} ${formMessageId}` : `${formDescriptionId}` + } + aria-invalid={!!error} + {...props} + /> + ); }); FormControl.displayName = "FormControl"; const FormDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLParagraphElement> + HTMLParagraphElement, + React.HTMLAttributes<HTMLParagraphElement> >(({ className, ...props }, ref) => { - const { formDescriptionId } = useFormField(); + const { formDescriptionId } = useFormField(); - return ( - <p - ref={ref} - id={formDescriptionId} - className={cn("text-[0.8rem] text-muted-foreground", className)} - {...props} - /> - ); + return ( + <p + ref={ref} + id={formDescriptionId} + className={cn("text-muted-foreground text-[0.8rem]", className)} + {...props} + /> + ); }); FormDescription.displayName = "FormDescription"; const FormMessage = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLParagraphElement> + HTMLParagraphElement, + React.HTMLAttributes<HTMLParagraphElement> >(({ className, children, ...props }, ref) => { - const { error, formMessageId } = useFormField(); - const body = error ? String(error?.message) : children; + const { error, formMessageId } = useFormField(); + const body = error ? String(error?.message) : children; - if (!body) { - return null; - } + if (!body) { + return null; + } - return ( - <p - ref={ref} - id={formMessageId} - className={cn("text-[0.8rem] font-medium text-destructive", className)} - {...props} - > - {body} - </p> - ); + return ( + <p + ref={ref} + id={formMessageId} + className={cn("text-destructive text-[0.8rem] font-medium", className)} + {...props} + > + {body} + </p> + ); }); FormMessage.displayName = "FormMessage"; export { - useFormField, - Form, - FormItem, - FormLabel, - FormControl, - FormDescription, - FormMessage, - FormField, + useFormField, + Form, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, + FormField, }; diff --git a/apps/web/src/components/ui/hover-card.tsx b/apps/web/src/components/ui/hover-card.tsx index 92700e09..6f2c34e1 100644 --- a/apps/web/src/components/ui/hover-card.tsx +++ b/apps/web/src/components/ui/hover-card.tsx @@ -3,26 +3,26 @@ import * as React from "react"; import { HoverCard as HoverCardPrimitive } from "radix-ui"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const HoverCard = HoverCardPrimitive.Root; const HoverCardTrigger = HoverCardPrimitive.Trigger; const HoverCardContent = React.forwardRef< - React.ElementRef<typeof HoverCardPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> + React.ElementRef<typeof HoverCardPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( - <HoverCardPrimitive.Content - ref={ref} - align={align} - sideOffset={sideOffset} - className={cn( - "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden 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", - className - )} - {...props} - /> + <HoverCardPrimitive.Content + ref={ref} + align={align} + sideOffset={sideOffset} + 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 z-50 w-64 rounded-md border p-4 shadow-md outline-hidden", + className, + )} + {...props} + /> )); HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; diff --git a/apps/web/src/components/ui/image-timeline-treatment.tsx b/apps/web/src/components/ui/image-timeline-treatment.tsx deleted file mode 100644 index 7a64be18..00000000 --- a/apps/web/src/components/ui/image-timeline-treatment.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { cn } from "@/lib/utils"; -import type { BackgroundType } from "@/types/editor"; - -interface ImageTimelineTreatmentProps { - src: string; - alt: string; - targetAspectRatio?: number; // Default to 16:9 for video - className?: string; - backgroundType?: BackgroundType; - backgroundColor?: string; -} - -export function ImageTimelineTreatment({ - src, - alt, - targetAspectRatio = 16 / 9, - className, - backgroundType = "blur", - backgroundColor = "#000000", -}: ImageTimelineTreatmentProps) { - const [imageLoaded, setImageLoaded] = useState(false); - const [imageDimensions, setImageDimensions] = useState<{ - width: number; - height: number; - } | null>(null); - - const handleImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => { - const img = e.currentTarget; - setImageDimensions({ - width: img.naturalWidth, - height: img.naturalHeight, - }); - setImageLoaded(true); - }; - - const imageAspectRatio = imageDimensions - ? imageDimensions.width / imageDimensions.height - : 1; - - const needsAspectRatioTreatment = imageAspectRatio !== targetAspectRatio; - - return ( - <div - className={cn("relative overflow-hidden", className)} - style={{ aspectRatio: targetAspectRatio }} - > - {/* Background Layer */} - {needsAspectRatioTreatment && imageLoaded && ( - <> - {backgroundType === "blur" && ( - <div className="absolute inset-0"> - <img - src={src} - alt="" - className="w-full h-full object-cover filter blur-xl scale-110 opacity-60" - aria-hidden="true" - /> - <div className="absolute inset-0 bg-black/20" /> - </div> - )} - - {backgroundType === "mirror" && ( - <div className="absolute inset-0"> - <img - src={src} - alt="" - className="w-full h-full object-cover opacity-30" - aria-hidden="true" - /> - </div> - )} - - {backgroundType === "color" && ( - <div className="absolute inset-0" style={{ backgroundColor }} /> - )} - </> - )} - - {/* Main Image Layer */} - <div className="absolute inset-0"> - <img - src={src} - alt={alt} - className="w-full h-full object-cover" - onLoad={handleImageLoad} - /> - </div> - - {/* Loading state */} - {!imageLoaded && ( - <div className="absolute inset-0 flex items-center justify-center bg-muted/30"> - <div className="animate-pulse text-xs text-muted-foreground"> - Loading... - </div> - </div> - )} - </div> - ); -} diff --git a/apps/web/src/components/ui/input-otp.tsx b/apps/web/src/components/ui/input-otp.tsx index 0d2cc002..ec71f96a 100644 --- a/apps/web/src/components/ui/input-otp.tsx +++ b/apps/web/src/components/ui/input-otp.tsx @@ -4,71 +4,71 @@ import * as React from "react"; import { OTPInput, OTPInputContext } from "input-otp"; import { Minus } from "lucide-react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const InputOTP = React.forwardRef< - React.ElementRef<typeof OTPInput>, - React.ComponentPropsWithoutRef<typeof OTPInput> + React.ElementRef<typeof OTPInput>, + React.ComponentPropsWithoutRef<typeof OTPInput> >(({ className, containerClassName, ...props }, ref) => ( - <OTPInput - ref={ref} - containerClassName={cn( - "flex items-center justify-between gap-2 w-full has-disabled:opacity-50", - containerClassName - )} - className={cn("disabled:cursor-not-allowed", className)} - {...props} - /> + <OTPInput + ref={ref} + containerClassName={cn( + "flex items-center justify-between gap-2 w-full has-disabled:opacity-50", + containerClassName, + )} + className={cn("disabled:cursor-not-allowed", className)} + {...props} + /> )); InputOTP.displayName = "InputOTP"; const InputOTPGroup = React.forwardRef< - React.ElementRef<"div">, - React.ComponentPropsWithoutRef<"div"> + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> >(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("flex items-center flex-1 gap-2", className)} - {...props} - /> + <div + ref={ref} + className={cn("flex flex-1 items-center gap-2", className)} + {...props} + /> )); InputOTPGroup.displayName = "InputOTPGroup"; const InputOTPSlot = React.forwardRef< - React.ElementRef<"div">, - React.ComponentPropsWithoutRef<"div"> & { index: number } + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> & { index: number } >(({ index, className, ...props }, ref) => { - const inputOTPContext = React.useContext(OTPInputContext); - const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]; + const inputOTPContext = React.useContext(OTPInputContext); + const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]; - return ( - <div - ref={ref} - className={cn( - "relative flex aspect-square flex-1 min-w-[36px] items-center justify-center border border-input text-lg shadow-xs transition-all first:rounded-l-md first:border-l last:rounded-r-md", - isActive && "z-10 ring-1 ring-ring", - className - )} - {...props} - > - {char} - {hasFakeCaret && ( - <div className="pointer-events-none absolute inset-0 flex items-center justify-center"> - <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" /> - </div> - )} - </div> - ); + return ( + <div + ref={ref} + className={cn( + "border-input relative flex aspect-square min-w-[36px] flex-1 items-center justify-center border text-lg shadow-xs first:rounded-l-md first:border-l last:rounded-r-md", + isActive && "ring-ring z-10 ring-1", + className, + )} + {...props} + > + {char} + {hasFakeCaret && ( + <div className="pointer-events-none absolute inset-0 flex items-center justify-center"> + <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" /> + </div> + )} + </div> + ); }); InputOTPSlot.displayName = "InputOTPSlot"; const InputOTPSeparator = React.forwardRef< - React.ElementRef<"div">, - React.ComponentPropsWithoutRef<"div"> + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> >(({ ...props }, ref) => ( - <div ref={ref} role="separator" {...props}> - <Minus /> - </div> + <div ref={ref} role="separator" {...props}> + <Minus /> + </div> )); InputOTPSeparator.displayName = "InputOTPSeparator"; diff --git a/apps/web/src/components/ui/input-with-back.tsx b/apps/web/src/components/ui/input-with-back.tsx index cae9f8c9..392ab3c3 100644 --- a/apps/web/src/components/ui/input-with-back.tsx +++ b/apps/web/src/components/ui/input-with-back.tsx @@ -7,82 +7,82 @@ import { motion } from "motion/react"; import { useState, useEffect } from "react";
interface InputWithBackProps {
- isExpanded: boolean;
- setIsExpanded: (isExpanded: boolean) => void;
- placeholder?: string;
- value?: string;
- onChange?: (value: string) => void;
- disableAnimation?: boolean;
+ isExpanded: boolean;
+ setIsExpanded: (isExpanded: boolean) => void;
+ placeholder?: string;
+ value?: string;
+ onChange?: (value: string) => void;
+ disableAnimation?: boolean;
}
export function InputWithBack({
- isExpanded,
- setIsExpanded,
- placeholder = "Search anything",
- value,
- onChange,
- disableAnimation = false,
+ isExpanded,
+ setIsExpanded,
+ placeholder = "Search anything",
+ value,
+ onChange,
+ disableAnimation = false,
}: InputWithBackProps) {
- const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null);
- const [buttonOffset, setButtonOffset] = useState(-60);
+ const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null);
+ const [buttonOffset, setButtonOffset] = useState(-60);
- const smoothTransition = {
- duration: disableAnimation ? 0 : 0.35,
- ease: [0.25, 0.1, 0.25, 1] as const,
- };
+ const smoothTransition = {
+ duration: disableAnimation ? 0 : 0.35,
+ ease: [0.25, 0.1, 0.25, 1] as const,
+ };
- useEffect(() => {
- if (containerRef) {
- const rect = containerRef.getBoundingClientRect();
- setButtonOffset(-rect.left - 48);
- }
- }, [containerRef]);
+ useEffect(() => {
+ if (containerRef) {
+ const rect = containerRef.getBoundingClientRect();
+ setButtonOffset(-rect.left - 48);
+ }
+ }, [containerRef]);
- return (
- <div ref={setContainerRef} className="relative w-full">
- <motion.div
- className="absolute left-0 top-1/2 -translate-y-1/2 cursor-pointer hover:opacity-75 transition-opacity z-10"
- initial={{
- x: isExpanded ? 0 : buttonOffset,
- opacity: isExpanded ? 1 : 0.5,
- }}
- animate={{
- x: isExpanded ? 0 : buttonOffset,
- opacity: isExpanded ? 1 : 0.5,
- }}
- transition={smoothTransition}
- onClick={() => setIsExpanded(!isExpanded)}
- >
- <Button
- variant="outline"
- className="!size-9 rounded-full bg-panel-accent"
- >
- <ArrowLeft />
- </Button>
- </motion.div>
- <div
- className="relative flex-1"
- style={{ marginLeft: "0px", paddingLeft: "0px" }}
- >
- <motion.div
- className="relative"
- initial={{
- marginLeft: isExpanded ? 50 : 0,
- }}
- animate={{
- marginLeft: isExpanded ? 50 : 0,
- }}
- transition={smoothTransition}
- >
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
- <Input
- placeholder={placeholder}
- className="pl-9 bg-panel-accent w-full"
- value={value}
- onChange={(e) => onChange?.(e.target.value)}
- />
- </motion.div>
- </div>
- </div>
- );
+ return (
+ <div ref={setContainerRef} className="relative w-full">
+ <motion.div
+ className="absolute top-1/2 left-0 z-10 -translate-y-1/2 cursor-pointer hover:opacity-75"
+ initial={{
+ x: isExpanded ? 0 : buttonOffset,
+ opacity: isExpanded ? 1 : 0.5,
+ }}
+ animate={{
+ x: isExpanded ? 0 : buttonOffset,
+ opacity: isExpanded ? 1 : 0.5,
+ }}
+ transition={smoothTransition}
+ onClick={() => setIsExpanded(!isExpanded)}
+ >
+ <Button
+ variant="outline"
+ className="bg-panel-accent !size-9 rounded-full"
+ >
+ <ArrowLeft />
+ </Button>
+ </motion.div>
+ <div
+ className="relative flex-1"
+ style={{ marginLeft: "0px", paddingLeft: "0px" }}
+ >
+ <motion.div
+ className="relative"
+ initial={{
+ marginLeft: isExpanded ? 50 : 0,
+ }}
+ animate={{
+ marginLeft: isExpanded ? 50 : 0,
+ }}
+ transition={smoothTransition}
+ >
+ <Search className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
+ <Input
+ placeholder={placeholder}
+ className="bg-panel-accent w-full pl-9"
+ value={value}
+ onChange={(e) => onChange?.(e.target.value)}
+ />
+ </motion.div>
+ </div>
+ </div>
+ );
}
diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx index 8f195a50..9be4bca2 100644 --- a/apps/web/src/components/ui/input.tsx +++ b/apps/web/src/components/ui/input.tsx @@ -1,113 +1,142 @@ "use client"; import { Eye, EyeOff, X } from "lucide-react"; -import { cn } from "../../lib/utils"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/utils/ui"; import { Button } from "./button"; -import { forwardRef, ComponentProps } from "react"; +import { forwardRef, type ComponentProps } from "react"; import { useState } from "react"; -interface InputProps extends ComponentProps<"input"> { - showPassword?: boolean; - onShowPasswordChange?: (show: boolean) => void; - showClearIcon?: boolean; - onClear?: () => void; - containerClassName?: string; +const inputVariants = cva( + "file:text-foreground placeholder:text-muted-foreground border-border bg-background flex w-full min-w-0 rounded-md border shadow-xs outline-none file:inline-flex file:border-0 file:bg-transparent file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:ring-4 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "selection:bg-primary selection:text-primary-foreground focus-visible:border-primary focus-visible:ring-primary/10", + destructive: "selection:bg-destructive selection:text-destructive-foreground focus-visible:border-destructive focus-visible:ring-destructive/10", + }, + size: { + default: + "h-9 px-3 py-1 text-base file:h-7 file:text-sm md:text-sm", + sm: "h-8 px-3 text-xs file:h-6 file:text-xs", + lg: "h-10 px-4 text-base file:h-8 file:text-sm md:text-sm", + }, + }, + defaultVariants: { + size: "default", + variant: "default", + }, + }, +); + +interface InputProps + extends Omit<ComponentProps<"input">, "size">, + VariantProps<typeof inputVariants> { + showPassword?: boolean; + onShowPasswordChange?: (show: boolean) => void; + showClearIcon?: boolean; + onClear?: () => void; + containerClassName?: string; } const Input = forwardRef<HTMLInputElement, InputProps>( - ( - { - className, - type, - containerClassName, - showPassword, - onShowPasswordChange, - showClearIcon, - onClear, - value, - onFocus, - onBlur, - ...props - }, - ref - ) => { - const [isFocused, setIsFocused] = useState(false); + ( + { + className, + type, + size, + variant, + containerClassName, + showPassword, + onShowPasswordChange, + showClearIcon, + onClear, + value, + onFocus, + onBlur, + ...props + }, + ref, + ) => { + const [isFocused, setIsFocused] = useState(false); - const isPassword = type === "password"; - const showPasswordToggle = isPassword && onShowPasswordChange; - const showClear = - showClearIcon && - onClear && - value && - String(value).length > 0 && - isFocused; - const inputType = isPassword && showPassword ? "text" : type; + const isPassword = type === "password"; + const showPasswordToggle = isPassword && onShowPasswordChange; + const showClear = + showClearIcon && + onClear && + value && + String(value).length > 0 && + isFocused; + const inputType = isPassword && showPassword ? "text" : type; - const hasIcons = showPasswordToggle || showClear; - const iconCount = Number(showPasswordToggle) + Number(showClear); - const paddingRight = - iconCount === 2 ? "pr-20" : iconCount === 1 ? "pr-10" : ""; + const hasIcons = showPasswordToggle || showClear; + const iconCount = Number(showPasswordToggle) + Number(showClear); + const paddingRight = + iconCount === 2 ? "pr-20" : iconCount === 1 ? "pr-10" : ""; - return ( - <div className={cn(hasIcons ? "relative w-full" : "", containerClassName)}> - <input - type={inputType} - className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-accent/50 px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]", - "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - paddingRight, - className - )} - ref={ref} - value={value} - onFocus={(e) => { - setIsFocused(true); - onFocus?.(e); - }} - onBlur={(e) => { - setIsFocused(false); - onBlur?.(e); - }} - {...props} - /> - {showClear && ( - <Button - type="button" - variant="text" - size="icon" - onMouseDown={(e) => { - e.preventDefault(); - onClear?.(); - }} - className="absolute right-0 top-0 h-full px-3 text-muted-foreground !opacity-100" - aria-label="Clear input" - > - <X className="!size-[0.85]" /> - </Button> - )} - {showPasswordToggle && ( - <Button - type="button" - variant="text" - size="icon" - onClick={() => onShowPasswordChange?.(!showPassword)} - className={cn( - "absolute top-0 h-full px-3 text-muted-foreground hover:text-foreground", - showClear ? "right-10" : "right-0" - )} - aria-label={showPassword ? "Hide password" : "Show password"} - > - {showPassword ? ( - <Eye className="h-4 w-4" /> - ) : ( - <EyeOff className="h-4 w-4" /> - )} - </Button> - )} - </div> - ); - } + return ( + <div + className={cn(hasIcons ? "relative w-full" : "", containerClassName)} + > + <input + type={inputType} + className={cn( + inputVariants({ + size, + className: cn(paddingRight, className), + variant, + }), + )} + ref={ref} + value={value} + onFocus={(e) => { + setIsFocused(true); + onFocus?.(e); + }} + onBlur={(e) => { + setIsFocused(false); + onBlur?.(e); + }} + {...props} + /> + {showClear && ( + <Button + type="button" + variant="text" + size="icon" + onMouseDown={(e) => { + e.preventDefault(); + onClear?.(); + }} + className="text-muted-foreground absolute top-0 right-0 h-full px-3 !opacity-100" + aria-label="Clear input" + > + <X className="!size-[0.85]" /> + </Button> + )} + {showPasswordToggle && ( + <Button + type="button" + variant="text" + size="icon" + onClick={() => onShowPasswordChange?.(!showPassword)} + className={cn( + "text-muted-foreground hover:text-foreground absolute top-0 h-full px-3", + showClear ? "right-10" : "right-0", + )} + aria-label={showPassword ? "Hide password" : "Show password"} + > + {showPassword ? ( + <Eye className="size-4" /> + ) : ( + <EyeOff className="size-4" /> + )} + </Button> + )} + </div> + ); + }, ); Input.displayName = "Input"; diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx index 877af15b..0ea7be03 100644 --- a/apps/web/src/components/ui/label.tsx +++ b/apps/web/src/components/ui/label.tsx @@ -4,22 +4,22 @@ import * as React from "react"; import { Label as LabelPrimitive } from "radix-ui"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" + "text-sm text-muted-foreground font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", ); const Label = React.forwardRef< - React.ElementRef<typeof LabelPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & - VariantProps<typeof labelVariants> + React.ElementRef<typeof LabelPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & + VariantProps<typeof labelVariants> >(({ className, ...props }, ref) => ( - <LabelPrimitive.Root - ref={ref} - className={cn(labelVariants(), className)} - {...props} - /> + <LabelPrimitive.Root + ref={ref} + className={cn(labelVariants(), className)} + {...props} + /> )); Label.displayName = LabelPrimitive.Root.displayName; diff --git a/apps/web/src/components/ui/menubar.tsx b/apps/web/src/components/ui/menubar.tsx index 96e66c8a..17a93839 100644 --- a/apps/web/src/components/ui/menubar.tsx +++ b/apps/web/src/components/ui/menubar.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import { Menubar as MenubarPrimitive } from "radix-ui"; import { Check, ChevronRight, Circle } from "lucide-react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const MenubarMenu = MenubarPrimitive.Menu; @@ -17,220 +17,220 @@ const MenubarSub = MenubarPrimitive.Sub; const MenubarRadioGroup = MenubarPrimitive.RadioGroup; const Menubar = React.forwardRef< - React.ElementRef<typeof MenubarPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root> + React.ElementRef<typeof MenubarPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root> >(({ className, ...props }, ref) => ( - <MenubarPrimitive.Root - ref={ref} - className={cn( - "flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-xs", - className - )} - {...props} - /> + <MenubarPrimitive.Root + ref={ref} + className={cn( + "bg-background flex h-9 items-center space-x-1 rounded-md border p-1 shadow-xs", + className, + )} + {...props} + /> )); Menubar.displayName = MenubarPrimitive.Root.displayName; const MenubarTrigger = React.forwardRef< - React.ElementRef<typeof MenubarPrimitive.Trigger>, - React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger> + React.ElementRef<typeof MenubarPrimitive.Trigger>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger> >(({ className, ...props }, ref) => ( - <MenubarPrimitive.Trigger - ref={ref} - className={cn( - "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-hidden focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", - className - )} - {...props} - /> + <MenubarPrimitive.Trigger + ref={ref} + className={cn( + "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-3 py-1 text-sm font-medium outline-hidden select-none", + className, + )} + {...props} + /> )); MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName; const MenubarSubTrigger = React.forwardRef< - React.ElementRef<typeof MenubarPrimitive.SubTrigger>, - React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & { - inset?: boolean; - } + React.ElementRef<typeof MenubarPrimitive.SubTrigger>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & { + inset?: boolean; + } >(({ className, inset, children, ...props }, ref) => ( - <MenubarPrimitive.SubTrigger - ref={ref} - className={cn( - "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground", - inset && "pl-8", - className - )} - {...props} - > - {children} - <ChevronRight className="ml-auto h-4 w-4" /> - </MenubarPrimitive.SubTrigger> + <MenubarPrimitive.SubTrigger + ref={ref} + className={cn( + "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none", + inset && "pl-8", + className, + )} + {...props} + > + {children} + <ChevronRight className="ml-auto size-4" /> + </MenubarPrimitive.SubTrigger> )); MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName; const MenubarSubContent = React.forwardRef< - React.ElementRef<typeof MenubarPrimitive.SubContent>, - React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent> + React.ElementRef<typeof MenubarPrimitive.SubContent>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent> >(({ className, ...props }, ref) => ( - <MenubarPrimitive.SubContent - ref={ref} - className={cn( - "z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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", - className - )} - {...props} - /> + <MenubarPrimitive.SubContent + ref={ref} + 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 z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-lg", + className, + )} + {...props} + /> )); MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName; const MenubarContent = React.forwardRef< - React.ElementRef<typeof MenubarPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content> + React.ElementRef<typeof MenubarPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content> >( - ( - { className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, - ref - ) => ( - <MenubarPrimitive.Portal> - <MenubarPrimitive.Content - ref={ref} - align={align} - alignOffset={alignOffset} - sideOffset={sideOffset} - className={cn( - "z-50 min-w-48 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in 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", - className - )} - {...props} - /> - </MenubarPrimitive.Portal> - ) + ( + { className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, + ref, + ) => ( + <MenubarPrimitive.Portal> + <MenubarPrimitive.Content + ref={ref} + align={align} + alignOffset={alignOffset} + sideOffset={sideOffset} + className={cn( + "bg-popover text-popover-foreground data-[state=open]:animate-in 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 z-50 min-w-48 overflow-hidden rounded-md border p-1 shadow-md", + className, + )} + {...props} + /> + </MenubarPrimitive.Portal> + ), ); MenubarContent.displayName = MenubarPrimitive.Content.displayName; const MenubarItem = React.forwardRef< - React.ElementRef<typeof MenubarPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & { - inset?: boolean; - } + React.ElementRef<typeof MenubarPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & { + inset?: boolean; + } >(({ className, inset, ...props }, ref) => ( - <MenubarPrimitive.Item - ref={ref} - className={cn( - "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", - inset && "pl-8", - className - )} - {...props} - /> + <MenubarPrimitive.Item + ref={ref} + className={cn( + "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50", + inset && "pl-8", + className, + )} + {...props} + /> )); MenubarItem.displayName = MenubarPrimitive.Item.displayName; const MenubarCheckboxItem = React.forwardRef< - React.ElementRef<typeof MenubarPrimitive.CheckboxItem>, - React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem> + React.ElementRef<typeof MenubarPrimitive.CheckboxItem>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem> >(({ className, children, checked, ...props }, ref) => ( - <MenubarPrimitive.CheckboxItem - ref={ref} - className={cn( - "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", - className - )} - checked={checked} - {...props} - > - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <MenubarPrimitive.ItemIndicator> - <Check className="h-4 w-4" /> - </MenubarPrimitive.ItemIndicator> - </span> - {children} - </MenubarPrimitive.CheckboxItem> + <MenubarPrimitive.CheckboxItem + ref={ref} + className={cn( + "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50", + className, + )} + checked={checked} + {...props} + > + <span className="absolute left-2 flex size-3.5 items-center justify-center"> + <MenubarPrimitive.ItemIndicator> + <Check className="size-4" /> + </MenubarPrimitive.ItemIndicator> + </span> + {children} + </MenubarPrimitive.CheckboxItem> )); MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName; const MenubarRadioItem = React.forwardRef< - React.ElementRef<typeof MenubarPrimitive.RadioItem>, - React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem> + React.ElementRef<typeof MenubarPrimitive.RadioItem>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem> >(({ className, children, ...props }, ref) => ( - <MenubarPrimitive.RadioItem - ref={ref} - className={cn( - "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50", - className - )} - {...props} - > - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <MenubarPrimitive.ItemIndicator> - <Circle className="h-4 w-4 fill-current" /> - </MenubarPrimitive.ItemIndicator> - </span> - {children} - </MenubarPrimitive.RadioItem> + <MenubarPrimitive.RadioItem + ref={ref} + className={cn( + "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50", + className, + )} + {...props} + > + <span className="absolute left-2 flex size-3.5 items-center justify-center"> + <MenubarPrimitive.ItemIndicator> + <Circle className="size-4 fill-current" /> + </MenubarPrimitive.ItemIndicator> + </span> + {children} + </MenubarPrimitive.RadioItem> )); MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName; const MenubarLabel = React.forwardRef< - React.ElementRef<typeof MenubarPrimitive.Label>, - React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & { - inset?: boolean; - } + React.ElementRef<typeof MenubarPrimitive.Label>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & { + inset?: boolean; + } >(({ className, inset, ...props }, ref) => ( - <MenubarPrimitive.Label - ref={ref} - className={cn( - "px-2 py-1.5 text-sm font-semibold", - inset && "pl-8", - className - )} - {...props} - /> + <MenubarPrimitive.Label + ref={ref} + className={cn( + "px-2 py-1.5 text-sm font-semibold", + inset && "pl-8", + className, + )} + {...props} + /> )); MenubarLabel.displayName = MenubarPrimitive.Label.displayName; const MenubarSeparator = React.forwardRef< - React.ElementRef<typeof MenubarPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator> + React.ElementRef<typeof MenubarPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator> >(({ className, ...props }, ref) => ( - <MenubarPrimitive.Separator - ref={ref} - className={cn("-mx-1 my-1 h-px bg-muted", className)} - {...props} - /> + <MenubarPrimitive.Separator + ref={ref} + className={cn("bg-muted -mx-1 my-1 h-px", className)} + {...props} + /> )); MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName; const MenubarShortcut = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn( - "ml-auto text-xs tracking-widest text-muted-foreground", - className - )} - {...props} - /> - ); + return ( + <span + className={cn( + "text-muted-foreground ml-auto text-xs tracking-widest", + className, + )} + {...props} + /> + ); }; MenubarShortcut.displayname = "MenubarShortcut"; export { - Menubar, - MenubarMenu, - MenubarTrigger, - MenubarContent, - MenubarItem, - MenubarSeparator, - MenubarLabel, - MenubarCheckboxItem, - MenubarRadioGroup, - MenubarRadioItem, - MenubarPortal, - MenubarSubContent, - MenubarSubTrigger, - MenubarGroup, - MenubarSub, - MenubarShortcut, + Menubar, + MenubarMenu, + MenubarTrigger, + MenubarContent, + MenubarItem, + MenubarSeparator, + MenubarLabel, + MenubarCheckboxItem, + MenubarRadioGroup, + MenubarRadioItem, + MenubarPortal, + MenubarSubContent, + MenubarSubTrigger, + MenubarGroup, + MenubarSub, + MenubarShortcut, }; diff --git a/apps/web/src/components/ui/navigation-menu.tsx b/apps/web/src/components/ui/navigation-menu.tsx index 288b5d88..f619d575 100644 --- a/apps/web/src/components/ui/navigation-menu.tsx +++ b/apps/web/src/components/ui/navigation-menu.tsx @@ -3,126 +3,126 @@ import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui"; import { cva } from "class-variance-authority"; import { ChevronDown } from "lucide-react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const NavigationMenu = React.forwardRef< - React.ElementRef<typeof NavigationMenuPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root> + React.ElementRef<typeof NavigationMenuPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root> >(({ className, children, ...props }, ref) => ( - <NavigationMenuPrimitive.Root - ref={ref} - className={cn( - "relative z-10 flex max-w-max flex-1 items-center justify-center", - className - )} - {...props} - > - {children} - <NavigationMenuViewport /> - </NavigationMenuPrimitive.Root> + <NavigationMenuPrimitive.Root + ref={ref} + className={cn( + "relative z-10 flex max-w-max flex-1 items-center justify-center", + className, + )} + {...props} + > + {children} + <NavigationMenuViewport /> + </NavigationMenuPrimitive.Root> )); NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName; const NavigationMenuList = React.forwardRef< - React.ElementRef<typeof NavigationMenuPrimitive.List>, - React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List> + React.ElementRef<typeof NavigationMenuPrimitive.List>, + React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List> >(({ className, ...props }, ref) => ( - <NavigationMenuPrimitive.List - ref={ref} - className={cn( - "group flex flex-1 list-none items-center justify-center space-x-1", - className - )} - {...props} - /> + <NavigationMenuPrimitive.List + ref={ref} + className={cn( + "group flex flex-1 list-none items-center justify-center space-x-1", + className, + )} + {...props} + /> )); NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName; const NavigationMenuItem = NavigationMenuPrimitive.Item; const navigationMenuTriggerStyle = cva( - "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50" + "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-active:bg-accent/50 data-[state=open]:bg-accent/50", ); const NavigationMenuTrigger = React.forwardRef< - React.ElementRef<typeof NavigationMenuPrimitive.Trigger>, - React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger> + React.ElementRef<typeof NavigationMenuPrimitive.Trigger>, + React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger> >(({ className, children, ...props }, ref) => ( - <NavigationMenuPrimitive.Trigger - ref={ref} - className={cn(navigationMenuTriggerStyle(), "group", className)} - {...props} - > - {children}{" "} - <ChevronDown - className="relative top-px ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180" - aria-hidden="true" - /> - </NavigationMenuPrimitive.Trigger> + <NavigationMenuPrimitive.Trigger + ref={ref} + className={cn(navigationMenuTriggerStyle(), "group", className)} + {...props} + > + {children}{" "} + <ChevronDown + className="relative top-px ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180" + aria-hidden="true" + /> + </NavigationMenuPrimitive.Trigger> )); NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName; const NavigationMenuContent = React.forwardRef< - React.ElementRef<typeof NavigationMenuPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content> + React.ElementRef<typeof NavigationMenuPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content> >(({ className, ...props }, ref) => ( - <NavigationMenuPrimitive.Content - ref={ref} - className={cn( - "left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ", - className - )} - {...props} - /> + <NavigationMenuPrimitive.Content + ref={ref} + className={cn( + "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full md:absolute md:w-auto", + className, + )} + {...props} + /> )); NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName; const NavigationMenuLink = NavigationMenuPrimitive.Link; const NavigationMenuViewport = React.forwardRef< - React.ElementRef<typeof NavigationMenuPrimitive.Viewport>, - React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport> + React.ElementRef<typeof NavigationMenuPrimitive.Viewport>, + React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport> >(({ className, ...props }, ref) => ( - <div className={cn("absolute left-0 top-full flex justify-center")}> - <NavigationMenuPrimitive.Viewport - className={cn( - "origin-top-center relative mt-1.5 h-(--radix-navigation-menu-viewport-height) w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-(--radix-navigation-menu-viewport-width)", - className - )} - ref={ref} - {...props} - /> - </div> + <div className={cn("absolute top-full left-0 flex justify-center")}> + <NavigationMenuPrimitive.Viewport + className={cn( + "origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-(--radix-navigation-menu-viewport-height) w-full overflow-hidden rounded-md border shadow-sm md:w-(--radix-navigation-menu-viewport-width)", + className, + )} + ref={ref} + {...props} + /> + </div> )); NavigationMenuViewport.displayName = - NavigationMenuPrimitive.Viewport.displayName; + NavigationMenuPrimitive.Viewport.displayName; const NavigationMenuIndicator = React.forwardRef< - React.ElementRef<typeof NavigationMenuPrimitive.Indicator>, - React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator> + React.ElementRef<typeof NavigationMenuPrimitive.Indicator>, + React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator> >(({ className, ...props }, ref) => ( - <NavigationMenuPrimitive.Indicator - ref={ref} - className={cn( - "top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in", - className - )} - {...props} - > - <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" /> - </NavigationMenuPrimitive.Indicator> + <NavigationMenuPrimitive.Indicator + ref={ref} + className={cn( + "data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden", + className, + )} + {...props} + > + <div className="bg-border relative top-[60%] size-2 rotate-45 rounded-tl-sm shadow-md" /> + </NavigationMenuPrimitive.Indicator> )); NavigationMenuIndicator.displayName = - NavigationMenuPrimitive.Indicator.displayName; + NavigationMenuPrimitive.Indicator.displayName; export { - navigationMenuTriggerStyle, - NavigationMenu, - NavigationMenuList, - NavigationMenuItem, - NavigationMenuContent, - NavigationMenuTrigger, - NavigationMenuLink, - NavigationMenuIndicator, - NavigationMenuViewport, + navigationMenuTriggerStyle, + NavigationMenu, + NavigationMenuList, + NavigationMenuItem, + NavigationMenuContent, + NavigationMenuTrigger, + NavigationMenuLink, + NavigationMenuIndicator, + NavigationMenuViewport, }; diff --git a/apps/web/src/components/ui/pagination.tsx b/apps/web/src/components/ui/pagination.tsx index 61766cec..96eec969 100644 --- a/apps/web/src/components/ui/pagination.tsx +++ b/apps/web/src/components/ui/pagination.tsx @@ -1,117 +1,117 @@ import * as React from "react"; import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"; -import { cn } from "../../lib/utils"; -import { ButtonProps, buttonVariants } from "./button"; +import { cn } from "@/utils/ui"; +import { type ButtonProps, buttonVariants } from "./button"; const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( - <nav - role="navigation" - aria-label="pagination" - className={cn("mx-auto flex w-full justify-center", className)} - {...props} - /> + <nav + role="navigation" + aria-label="pagination" + className={cn("mx-auto flex w-full justify-center", className)} + {...props} + /> ); Pagination.displayName = "Pagination"; const PaginationContent = React.forwardRef< - HTMLUListElement, - React.ComponentProps<"ul"> + HTMLUListElement, + React.ComponentProps<"ul"> >(({ className, ...props }, ref) => ( - <ul - ref={ref} - className={cn("flex flex-row items-center gap-1", className)} - {...props} - /> + <ul + ref={ref} + className={cn("flex flex-row items-center gap-1", className)} + {...props} + /> )); PaginationContent.displayName = "PaginationContent"; const PaginationItem = React.forwardRef< - HTMLLIElement, - React.ComponentProps<"li"> + HTMLLIElement, + React.ComponentProps<"li"> >(({ className, ...props }, ref) => ( - <li ref={ref} className={cn("", className)} {...props} /> + <li ref={ref} className={cn("", className)} {...props} /> )); PaginationItem.displayName = "PaginationItem"; type PaginationLinkProps = { - isActive?: boolean; + isActive?: boolean; } & Pick<ButtonProps, "size"> & - React.ComponentProps<"a">; + React.ComponentProps<"a">; const PaginationLink = ({ - className, - isActive, - size = "icon", - ...props + className, + isActive, + size = "icon", + ...props }: PaginationLinkProps) => ( - <a - aria-current={isActive ? "page" : undefined} - className={cn( - buttonVariants({ - variant: isActive ? "outline" : "text", - size, - }), - className - )} - {...props} - /> + <a + aria-current={isActive ? "page" : undefined} + className={cn( + buttonVariants({ + variant: isActive ? "outline" : "text", + size, + }), + className, + )} + {...props} + /> ); PaginationLink.displayName = "PaginationLink"; const PaginationPrevious = ({ - className, - ...props + className, + ...props }: React.ComponentProps<typeof PaginationLink>) => ( - <PaginationLink - aria-label="Go to previous page" - size="default" - className={cn("gap-1 pl-2.5", className)} - {...props} - > - <ChevronLeft className="h-4 w-4" /> - <span>Previous</span> - </PaginationLink> + <PaginationLink + aria-label="Go to previous page" + size="default" + className={cn("gap-1 pl-2.5", className)} + {...props} + > + <ChevronLeft className="size-4" /> + <span>Previous</span> + </PaginationLink> ); PaginationPrevious.displayName = "PaginationPrevious"; const PaginationNext = ({ - className, - ...props + className, + ...props }: React.ComponentProps<typeof PaginationLink>) => ( - <PaginationLink - aria-label="Go to next page" - size="default" - className={cn("gap-1 pr-2.5", className)} - {...props} - > - <span>Next</span> - <ChevronRight className="h-4 w-4" /> - </PaginationLink> + <PaginationLink + aria-label="Go to next page" + size="default" + className={cn("gap-1 pr-2.5", className)} + {...props} + > + <span>Next</span> + <ChevronRight className="size-4" /> + </PaginationLink> ); PaginationNext.displayName = "PaginationNext"; const PaginationEllipsis = ({ - className, - ...props + className, + ...props }: React.ComponentProps<"span">) => ( - <span - aria-hidden - className={cn("flex h-9 w-9 items-center justify-center", className)} - {...props} - > - <MoreHorizontal className="h-4 w-4" /> - <span className="sr-only">More pages</span> - </span> + <span + aria-hidden + className={cn("flex size-9 items-center justify-center", className)} + {...props} + > + <MoreHorizontal className="size-4" /> + <span className="sr-only">More pages</span> + </span> ); PaginationEllipsis.displayName = "PaginationEllipsis"; export { - Pagination, - PaginationContent, - PaginationLink, - PaginationItem, - PaginationPrevious, - PaginationNext, - PaginationEllipsis, + Pagination, + PaginationContent, + PaginationLink, + PaginationItem, + PaginationPrevious, + PaginationNext, + PaginationEllipsis, }; diff --git a/apps/web/src/components/ui/phone-input.tsx b/apps/web/src/components/ui/phone-input.tsx index ca85df8f..1d0eeb07 100644 --- a/apps/web/src/components/ui/phone-input.tsx +++ b/apps/web/src/components/ui/phone-input.tsx @@ -5,163 +5,163 @@ import flags from "react-phone-number-input/flags"; import { Button } from "./button"; import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, } from "./command"; import { Input } from "./input"; import { Popover, PopoverContent, PopoverTrigger } from "./popover"; import { ScrollArea } from "./scroll-area"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; type PhoneInputProps = Omit< - React.ComponentProps<"input">, - "onChange" | "value" | "ref" + React.ComponentProps<"input">, + "onChange" | "value" | "ref" > & - Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & { - onChange?: (value: RPNInput.Value) => void; - }; + Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & { + onChange?: (value: RPNInput.Value) => void; + }; const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> = - React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>( - ({ className, onChange, defaultCountry = "US", ...props }, ref) => { - return ( - <RPNInput.default - ref={ref} - className={cn("flex", className)} - flagComponent={FlagComponent} - countrySelectComponent={CountrySelect} - inputComponent={InputComponent} - defaultCountry={defaultCountry} - international - addInternationalOption - smartCaret={false} - /** - * Handles the onChange event. - * - * react-phone-number-input might trigger the onChange event as undefined - * when a valid phone number is not entered. To prevent this, - * the value is coerced to an empty string. - * - * @param {E164Number | undefined} value - The entered value - */ - onChange={(value) => onChange?.(value || ("" as RPNInput.Value))} - {...props} - /> - ); - } - ); + React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>( + ({ className, onChange, defaultCountry = "US", ...props }, ref) => { + return ( + <RPNInput.default + ref={ref} + className={cn("flex", className)} + flagComponent={FlagComponent} + countrySelectComponent={CountrySelect} + inputComponent={InputComponent} + defaultCountry={defaultCountry} + international + addInternationalOption + smartCaret={false} + /** + * Handles the onChange event. + * + * react-phone-number-input might trigger the onChange event as undefined + * when a valid phone number is not entered. To prevent this, + * the value is coerced to an empty string. + * + * @param {E164Number | undefined} value - The entered value + */ + onChange={(value) => onChange?.(value || ("" as RPNInput.Value))} + {...props} + /> + ); + }, + ); PhoneInput.displayName = "PhoneInput"; const InputComponent = React.forwardRef< - HTMLInputElement, - React.ComponentProps<"input"> + HTMLInputElement, + React.ComponentProps<"input"> >(({ className, ...props }, ref) => ( - <Input - className={cn("rounded-e-lg rounded-s-none", className)} - {...props} - ref={ref} - /> + <Input + className={cn("rounded-s-none rounded-e-lg", className)} + {...props} + ref={ref} + /> )); InputComponent.displayName = "InputComponent"; type CountryEntry = { label: string; value: RPNInput.Country | undefined }; type CountrySelectProps = { - disabled?: boolean; - value: RPNInput.Country; - options: CountryEntry[]; - onChange: (country: RPNInput.Country) => void; + disabled?: boolean; + value: RPNInput.Country; + options: CountryEntry[]; + onChange: (country: RPNInput.Country) => void; }; const CountrySelect = ({ - disabled, - value: selectedCountry, - options: countryList, - onChange, + disabled, + value: selectedCountry, + options: countryList, + onChange, }: CountrySelectProps) => { - return ( - <Popover> - <PopoverTrigger asChild> - <Button - type="button" - variant="outline" - className="flex gap-1 rounded-e-none rounded-s-lg border-r-0 px-3 focus:z-10" - disabled={disabled} - > - <FlagComponent - country={selectedCountry} - countryName={selectedCountry} - /> - <ChevronsUpDown - className={cn( - "-mr-2 size-4 opacity-50", - disabled ? "hidden" : "opacity-100" - )} - /> - </Button> - </PopoverTrigger> - <PopoverContent className="w-[300px] p-0"> - <Command> - <CommandInput placeholder="Search country..." /> - <CommandList> - <ScrollArea className="h-72"> - <CommandEmpty>No country found.</CommandEmpty> - <CommandGroup> - {countryList.map(({ value, label }) => - value ? ( - <CountrySelectOption - key={value} - country={value} - countryName={label} - selectedCountry={selectedCountry} - onChange={onChange} - /> - ) : null - )} - </CommandGroup> - </ScrollArea> - </CommandList> - </Command> - </PopoverContent> - </Popover> - ); + return ( + <Popover> + <PopoverTrigger asChild> + <Button + type="button" + variant="outline" + className="flex gap-1 rounded-s-lg rounded-e-none border-r-0 px-3 focus:z-10" + disabled={disabled} + > + <FlagComponent + country={selectedCountry} + countryName={selectedCountry} + /> + <ChevronsUpDown + className={cn( + "-mr-2 opacity-50", + disabled ? "hidden" : "opacity-100", + )} + /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[300px] p-0"> + <Command> + <CommandInput placeholder="Search country..." /> + <CommandList> + <ScrollArea className="h-72"> + <CommandEmpty>No country found.</CommandEmpty> + <CommandGroup> + {countryList.map(({ value, label }) => + value ? ( + <CountrySelectOption + key={value} + country={value} + countryName={label} + selectedCountry={selectedCountry} + onChange={onChange} + /> + ) : null, + )} + </CommandGroup> + </ScrollArea> + </CommandList> + </Command> + </PopoverContent> + </Popover> + ); }; interface CountrySelectOptionProps extends RPNInput.FlagProps { - selectedCountry: RPNInput.Country; - onChange: (country: RPNInput.Country) => void; + selectedCountry: RPNInput.Country; + onChange: (country: RPNInput.Country) => void; } const CountrySelectOption = ({ - country, - countryName, - selectedCountry, - onChange, + country, + countryName, + selectedCountry, + onChange, }: CountrySelectOptionProps) => { - return ( - <CommandItem className="gap-2" onSelect={() => onChange(country)}> - <FlagComponent country={country} countryName={countryName} /> - <span className="flex-1 text-sm">{countryName}</span> - <span className="text-sm text-foreground/50">{`+${RPNInput.getCountryCallingCode(country)}`}</span> - <CheckIcon - className={`ml-auto size-4 ${country === selectedCountry ? "opacity-100" : "opacity-0"}`} - /> - </CommandItem> - ); + return ( + <CommandItem className="gap-2" onSelect={() => onChange(country)}> + <FlagComponent country={country} countryName={countryName} /> + <span className="flex-1 text-sm">{countryName}</span> + <span className="text-foreground/50 text-sm">{`+${RPNInput.getCountryCallingCode(country)}`}</span> + <CheckIcon + className={`ml-auto size-4 ${country === selectedCountry ? "opacity-100" : "opacity-0"}`} + /> + </CommandItem> + ); }; const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => { - const Flag = flags[country]; + const Flag = flags[country]; - return ( - <span className="flex h-4 w-6 overflow-hidden rounded-sm bg-foreground/20 [&_svg]:size-full"> - {Flag && <Flag title={countryName} />} - </span> - ); + return ( + <span className="bg-foreground/20 flex h-4 w-6 overflow-hidden rounded-sm [&_svg]:size-full"> + {Flag && <Flag title={countryName} />} + </span> + ); }; export { PhoneInput }; diff --git a/apps/web/src/components/ui/popover.tsx b/apps/web/src/components/ui/popover.tsx index 406ff8eb..34738303 100644 --- a/apps/web/src/components/ui/popover.tsx +++ b/apps/web/src/components/ui/popover.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import { Popover as PopoverPrimitive } from "radix-ui"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Popover = PopoverPrimitive.Root; @@ -12,21 +12,21 @@ const PopoverTrigger = PopoverPrimitive.Trigger; const PopoverAnchor = PopoverPrimitive.Anchor; const PopoverContent = React.forwardRef< - React.ElementRef<typeof PopoverPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> + React.ElementRef<typeof PopoverPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( - <PopoverPrimitive.Portal> - <PopoverPrimitive.Content - ref={ref} - align={align} - sideOffset={sideOffset} - className={cn( - "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-[0_0_10px_rgba(0,0,0,0.15)] outline-hidden 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", - className - )} - {...props} - /> - </PopoverPrimitive.Portal> + <PopoverPrimitive.Portal> + <PopoverPrimitive.Content + ref={ref} + align={align} + sideOffset={sideOffset} + 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 z-50 w-72 rounded-md border p-4 shadow-[0_0_10px_rgba(0,0,0,0.15)] outline-hidden", + className, + )} + {...props} + /> + </PopoverPrimitive.Portal> )); PopoverContent.displayName = PopoverPrimitive.Content.displayName; diff --git a/apps/web/src/components/ui/progress.tsx b/apps/web/src/components/ui/progress.tsx index 6b785a58..8bf5e939 100644 --- a/apps/web/src/components/ui/progress.tsx +++ b/apps/web/src/components/ui/progress.tsx @@ -3,25 +3,25 @@ import * as React from "react"; import { Progress as ProgressPrimitive } from "radix-ui"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Progress = React.forwardRef< - React.ElementRef<typeof ProgressPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> + React.ElementRef<typeof ProgressPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> >(({ className, value, ...props }, ref) => ( - <ProgressPrimitive.Root - ref={ref} - className={cn( - "relative h-2 w-full overflow-hidden rounded-full bg-accent", - className - )} - {...props} - > - <ProgressPrimitive.Indicator - className="h-full w-full flex-1 bg-foreground transition-all" - style={{ transform: `translateX(-${100 - (value || 0)}%)` }} - /> - </ProgressPrimitive.Root> + <ProgressPrimitive.Root + ref={ref} + className={cn( + "bg-accent relative h-2 w-full overflow-hidden rounded-full", + className, + )} + {...props} + > + <ProgressPrimitive.Indicator + className="bg-primary size-full flex-1" + style={{ transform: `translateX(-${100 - (value || 0)}%)` }} + /> + </ProgressPrimitive.Root> )); Progress.displayName = ProgressPrimitive.Root.displayName; diff --git a/apps/web/src/components/ui/prose.tsx b/apps/web/src/components/ui/prose.tsx index c25a5a65..7586344c 100644 --- a/apps/web/src/components/ui/prose.tsx +++ b/apps/web/src/components/ui/prose.tsx @@ -1,22 +1,22 @@ -import { cn } from "@/lib/utils"; +import { cn } from "@/utils/ui"; import type React from "react"; type ProseProps = React.HTMLAttributes<HTMLElement> & { - as?: "article"; - html: string; + as?: "article"; + html: string; }; function Prose({ children, html, className }: ProseProps) { - return ( - <article - className={cn( - "prose prose-h2:font-semibold max-w-none prose-h1:text-xl prose-a:text-blue-600 prose-p:text-justify dark:prose-invert mx-auto", - className - )} - > - {html ? <div dangerouslySetInnerHTML={{ __html: html }} /> : children} - </article> - ); + return ( + <article + className={cn( + "prose prose-h2:font-semibold prose-h1:text-xl prose-a:text-blue-600 prose-p:text-justify dark:prose-invert mx-auto max-w-none", + className, + )} + > + {html ? <div dangerouslySetInnerHTML={{ __html: html }} /> : children} + </article> + ); } export default Prose; diff --git a/apps/web/src/components/ui/radio-group.tsx b/apps/web/src/components/ui/radio-group.tsx index 859b8b5f..fe2045dc 100644 --- a/apps/web/src/components/ui/radio-group.tsx +++ b/apps/web/src/components/ui/radio-group.tsx @@ -4,40 +4,40 @@ import * as React from "react"; import { RadioGroup as RadioGroupPrimitive } from "radix-ui"; import { Circle } from "lucide-react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const RadioGroup = React.forwardRef< - React.ElementRef<typeof RadioGroupPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> + React.ElementRef<typeof RadioGroupPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> >(({ className, ...props }, ref) => { - return ( - <RadioGroupPrimitive.Root - className={cn("grid gap-2", className)} - {...props} - ref={ref} - /> - ); + return ( + <RadioGroupPrimitive.Root + className={cn("grid gap-2", className)} + {...props} + ref={ref} + /> + ); }); RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; const RadioGroupItem = React.forwardRef< - React.ElementRef<typeof RadioGroupPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> + React.ElementRef<typeof RadioGroupPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> >(({ className, ...props }, ref) => { - return ( - <RadioGroupPrimitive.Item - ref={ref} - className={cn( - "aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow-sm focus:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", - className - )} - {...props} - > - <RadioGroupPrimitive.Indicator className="flex items-center justify-center"> - <Circle className="h-3.5 w-3.5 fill-primary" /> - </RadioGroupPrimitive.Indicator> - </RadioGroupPrimitive.Item> - ); + return ( + <RadioGroupPrimitive.Item + ref={ref} + className={cn( + "border-primary text-primary focus-visible:ring-ring aspect-square size-4 rounded-full border shadow-sm focus:outline-hidden focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50", + className, + )} + {...props} + > + <RadioGroupPrimitive.Indicator className="flex items-center justify-center"> + <Circle className="fill-primary size-3.5" /> + </RadioGroupPrimitive.Indicator> + </RadioGroupPrimitive.Item> + ); }); RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; diff --git a/apps/web/src/components/ui/react-markdown-wrapper.tsx b/apps/web/src/components/ui/react-markdown-wrapper.tsx new file mode 100644 index 00000000..181ae8ac --- /dev/null +++ b/apps/web/src/components/ui/react-markdown-wrapper.tsx @@ -0,0 +1,26 @@ +import ReactMarkdown from "react-markdown"; +import { cn } from "@/utils/ui"; + +export function ReactMarkdownWrapper({ children }: { children: string }) { + return ( + <ReactMarkdown + components={{ + a: ({ className: linkClassName, children, ...props }) => ( + <a + className={cn("text-primary hover:underline", linkClassName)} + target="_blank" + rel="noopener noreferrer" + {...props} + > + {children} + </a> + ), + strong: ({ children }) => ( + <strong className="text-foreground font-semibold">{children}</strong> + ), + }} + > + {children} + </ReactMarkdown> + ); +} diff --git a/apps/web/src/components/ui/resizable.tsx b/apps/web/src/components/ui/resizable.tsx index fc9d8b81..9287797d 100644 --- a/apps/web/src/components/ui/resizable.tsx +++ b/apps/web/src/components/ui/resizable.tsx @@ -2,37 +2,37 @@ import * as ResizablePrimitive from "react-resizable-panels"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const ResizablePanelGroup = ({ - className, - ...props + className, + ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => ( - <ResizablePrimitive.PanelGroup - className={cn( - "flex h-full w-full data-[panel-group-direction=vertical]:flex-col", - className - )} - {...props} - /> + <ResizablePrimitive.PanelGroup + className={cn( + "flex size-full data-[panel-group-direction=vertical]:flex-col", + className, + )} + {...props} + /> ); const ResizablePanel = ResizablePrimitive.Panel; const ResizableHandle = ({ - withHandle, - className, - ...props + withHandle, + className, + ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & { - withHandle?: boolean; + withHandle?: boolean; }) => ( - <ResizablePrimitive.PanelResizeHandle - className={cn( - "relative flex w-px items-center justify-center bg-transparent after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90", - className - )} - {...props} - /> + <ResizablePrimitive.PanelResizeHandle + className={cn( + "focus-visible:ring-ring relative flex w-px items-center justify-center bg-transparent after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90", + className, + )} + {...props} + /> ); export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; diff --git a/apps/web/src/components/ui/scroll-area.tsx b/apps/web/src/components/ui/scroll-area.tsx index 1fc227b0..9019fae5 100644 --- a/apps/web/src/components/ui/scroll-area.tsx +++ b/apps/web/src/components/ui/scroll-area.tsx @@ -1,17 +1,17 @@ import * as React from "react"; -import { cn } from "@/lib/utils"; +import { cn } from "@/utils/ui"; const ScrollArea = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> + HTMLDivElement, + React.HTMLAttributes<HTMLDivElement> >(({ className, children, ...props }, ref) => ( - <div - ref={ref} - className={cn("overflow-auto scrollbar-thin", className)} - {...props} - > - {children} - </div> + <div + ref={ref} + className={cn("scrollbar-thin overflow-auto", className)} + {...props} + > + {children} + </div> )); ScrollArea.displayName = "ScrollArea"; diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx index 53ca27cb..79462d4a 100644 --- a/apps/web/src/components/ui/select.tsx +++ b/apps/web/src/components/ui/select.tsx @@ -5,7 +5,7 @@ import { Select as SelectPrimitive } from "radix-ui"; import { Check, ChevronDown, ChevronUp } from "lucide-react"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Select = SelectPrimitive.Root; @@ -14,165 +14,166 @@ const SelectGroup = SelectPrimitive.Group; const SelectValue = SelectPrimitive.Value; const selectItemVariants = cva( - "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-hidden transition-opacity data-disabled:pointer-events-none data-disabled:opacity-50", - { - variants: { - variant: { - default: "focus:opacity-65 focus:text-accent-foreground", - destructive: "text-destructive focus:text-destructive/80", - }, - }, - defaultVariants: { - variant: "default", - }, - } + "relative flex cursor-pointer select-none items-center gap-2 rounded-xl px-2.5 py-2 text-sm text-foreground/85 outline-hidden data-[highlighted]:bg-accent/35 data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0", + { + variants: { + variant: { + default: "", + destructive: "text-destructive data-[highlighted]:bg-destructive/5 data-[highlighted]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + }, ); const SelectTrigger = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Trigger>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> + React.ElementRef<typeof SelectPrimitive.Trigger>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> >(({ className, children, ...props }, ref) => ( - <SelectPrimitive.Trigger - ref={ref} - className={cn( - "flex gap-1 h-7 cursor-pointer w-auto items-center justify-between whitespace-nowrap rounded-md bg-accent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", - className - )} - {...props} - > - {children} - <SelectPrimitive.Icon asChild> - <ChevronDown className="size-3 opacity-50" /> - </SelectPrimitive.Icon> - </SelectPrimitive.Trigger> + <SelectPrimitive.Trigger + ref={ref} + className={cn( + "bg-accent ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-7 w-auto cursor-pointer items-center justify-between gap-1 rounded-md px-3 py-2 text-sm whitespace-nowrap focus:ring-1 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", + "focus:border-primary focus:ring-4 focus:ring-primary/10 border-transparent transition-none", + className, + )} + {...props} + > + {children} + <SelectPrimitive.Icon asChild> + <ChevronDown className="size-3 opacity-50" /> + </SelectPrimitive.Icon> + </SelectPrimitive.Trigger> )); SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; const SelectScrollUpButton = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> + React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> >(({ className, ...props }, ref) => ( - <SelectPrimitive.ScrollUpButton - ref={ref} - className={cn( - "flex cursor-default items-center justify-center py-1", - className - )} - {...props} - > - <ChevronUp className="h-4 w-4" /> - </SelectPrimitive.ScrollUpButton> + <SelectPrimitive.ScrollUpButton + ref={ref} + className={cn( + "flex cursor-default items-center justify-center py-1", + className, + )} + {...props} + > + <ChevronUp className="size-4" /> + </SelectPrimitive.ScrollUpButton> )); SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; const SelectScrollDownButton = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> + React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> >(({ className, ...props }, ref) => ( - <SelectPrimitive.ScrollDownButton - ref={ref} - className={cn( - "flex cursor-default items-center justify-center py-1", - className - )} - {...props} - > - <ChevronDown className="h-4 w-4" /> - </SelectPrimitive.ScrollDownButton> + <SelectPrimitive.ScrollDownButton + ref={ref} + className={cn( + "flex cursor-default items-center justify-center py-1", + className, + )} + {...props} + > + <ChevronDown className="size-4" /> + </SelectPrimitive.ScrollDownButton> )); SelectScrollDownButton.displayName = - SelectPrimitive.ScrollDownButton.displayName; + SelectPrimitive.ScrollDownButton.displayName; const SelectContent = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> + React.ElementRef<typeof SelectPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> >(({ className, children, position = "popper", ...props }, ref) => ( - <SelectPrimitive.Portal> - <SelectPrimitive.Content - ref={ref} - className={cn( - "relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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", - 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} - onCloseAutoFocus={(e) => { - e.preventDefault(); - e.stopPropagation(); - }} - {...props} - > - <SelectScrollUpButton /> - <SelectPrimitive.Viewport - className={cn( - "p-1", - position === "popper" && - "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)" - )} - > - {children} - </SelectPrimitive.Viewport> - <SelectScrollDownButton /> - </SelectPrimitive.Content> - </SelectPrimitive.Portal> + <SelectPrimitive.Portal> + <SelectPrimitive.Content + ref={ref} + className={cn( + "bg-popover text-popover-foreground z-50 max-h-96 min-w-32 overflow-hidden rounded-2xl border p-2 shadow-lg", + className, + )} + position={position} + onCloseAutoFocus={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} + {...props} + > + <SelectScrollUpButton /> + <SelectPrimitive.Viewport + className={cn( + position === "popper" && + "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)", + )} + > + {children} + </SelectPrimitive.Viewport> + <SelectScrollDownButton /> + </SelectPrimitive.Content> + </SelectPrimitive.Portal> )); SelectContent.displayName = SelectPrimitive.Content.displayName; const SelectLabel = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Label>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> + React.ElementRef<typeof SelectPrimitive.Label>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> >(({ className, ...props }, ref) => ( - <SelectPrimitive.Label - ref={ref} - className={cn("px-2 py-1.5 text-sm font-semibold", className)} - {...props} - /> + <SelectPrimitive.Label + ref={ref} + className={cn( + "px-3 pb-2 pt-1 text-[11px] font-bold uppercase tracking-wider text-muted-foreground", + className, + )} + {...props} + /> )); SelectLabel.displayName = SelectPrimitive.Label.displayName; const SelectItem = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & { - variant?: VariantProps<typeof selectItemVariants>["variant"]; - } + React.ElementRef<typeof SelectPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & { + variant?: VariantProps<typeof selectItemVariants>["variant"]; + } >(({ className, children, variant = "default", ...props }, ref) => ( - <SelectPrimitive.Item - ref={ref} - className={cn(selectItemVariants({ variant }), className)} - {...props} - > - <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center"> - <SelectPrimitive.ItemIndicator> - <Check className="h-4 w-4" /> - </SelectPrimitive.ItemIndicator> - </span> - <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> - </SelectPrimitive.Item> + <SelectPrimitive.Item + ref={ref} + className={cn(selectItemVariants({ variant }), "pr-8 pl-2", className)} + {...props} + > + <span className="absolute right-2 flex size-3.5 items-center justify-center"> + <SelectPrimitive.ItemIndicator> + <Check className="size-4" /> + </SelectPrimitive.ItemIndicator> + </span> + <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> + </SelectPrimitive.Item> )); SelectItem.displayName = SelectPrimitive.Item.displayName; const SelectSeparator = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> + React.ElementRef<typeof SelectPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> >(({ className, ...props }, ref) => ( - <SelectPrimitive.Separator - ref={ref} - className={cn("-mx-1 my-1 h-px bg-foreground/10", className)} - {...props} - /> + <SelectPrimitive.Separator + ref={ref} + className={cn("bg-border/60 mx-1 my-2 h-px", className)} + {...props} + /> )); SelectSeparator.displayName = SelectPrimitive.Separator.displayName; export { - Select, - SelectGroup, - SelectValue, - SelectTrigger, - SelectContent, - SelectLabel, - SelectItem, - SelectSeparator, - SelectScrollUpButton, - SelectScrollDownButton, + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, }; diff --git a/apps/web/src/components/ui/separator.tsx b/apps/web/src/components/ui/separator.tsx index f0040a52..f3f93ad7 100644 --- a/apps/web/src/components/ui/separator.tsx +++ b/apps/web/src/components/ui/separator.tsx @@ -3,28 +3,28 @@ import * as React from "react"; import * as SeparatorPrimitive from "@radix-ui/react-separator"; -import { cn } from "@/lib/utils"; +import { cn } from "@/utils/ui"; const Separator = React.forwardRef< - React.ElementRef<typeof SeparatorPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> + React.ElementRef<typeof SeparatorPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> >( - ( - { className, orientation = "horizontal", decorative = true, ...props }, - ref - ) => ( - <SeparatorPrimitive.Root - ref={ref} - decorative={decorative} - orientation={orientation} - className={cn( - "shrink-0 bg-border", - orientation === "horizontal" ? "h-px w-full" : "h-full w-px", - className - )} - {...props} - /> - ) + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref, + ) => ( + <SeparatorPrimitive.Root + ref={ref} + decorative={decorative} + orientation={orientation} + className={cn( + "bg-border shrink-0", + orientation === "horizontal" ? "h-px w-full" : "h-full w-px", + className, + )} + {...props} + /> + ), ); Separator.displayName = SeparatorPrimitive.Root.displayName; diff --git a/apps/web/src/components/ui/sheet.tsx b/apps/web/src/components/ui/sheet.tsx index f97bd438..3155787d 100644 --- a/apps/web/src/components/ui/sheet.tsx +++ b/apps/web/src/components/ui/sheet.tsx @@ -5,7 +5,7 @@ import { Dialog as SheetPrimitive } from "radix-ui"; import { cva, type VariantProps } from "class-variance-authority"; import { X } from "lucide-react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Sheet = SheetPrimitive.Root; @@ -16,129 +16,129 @@ const SheetClose = SheetPrimitive.Close; const SheetPortal = SheetPrimitive.Portal; const SheetOverlay = React.forwardRef< - React.ElementRef<typeof SheetPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay> + React.ElementRef<typeof SheetPrimitive.Overlay>, + React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay> >(({ className, ...props }, ref) => ( - <SheetPrimitive.Overlay - className={cn( - "fixed inset-0 z-200 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", - className - )} - {...props} - ref={ref} - /> + <SheetPrimitive.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-200 bg-black/50", + className, + )} + {...props} + ref={ref} + /> )); SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; const sheetVariants = cva( - "fixed z-250 gap-4 bg-background p-6 shadow-lg transition ease data-[state=closed]:duration-250 data-[state=open]:duration-250 data-[state=open]:animate-in data-[state=closed]:animate-out", - { - variants: { - side: { - top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", - bottom: - "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", - left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", - right: - "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", - }, - }, - defaultVariants: { - side: "right", - }, - } + "fixed z-250 gap-4 bg-background p-6 shadow-lg transition ease data-[state=closed]:duration-250 data-[state=open]:duration-250 data-[state=open]:animate-in data-[state=closed]:animate-out", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + }, ); interface SheetContentProps - extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, - VariantProps<typeof sheetVariants> {} + extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, + VariantProps<typeof sheetVariants> {} const SheetContent = React.forwardRef< - React.ElementRef<typeof SheetPrimitive.Content>, - SheetContentProps + React.ElementRef<typeof SheetPrimitive.Content>, + SheetContentProps >(({ side = "right", className, children, ...props }, ref) => ( - <SheetPortal> - <SheetOverlay /> - <SheetPrimitive.Content - ref={ref} - className={cn(sheetVariants({ side }), className)} - onOpenAutoFocus={(e) => { - e.preventDefault(); - e.stopPropagation(); - }} - {...props} - > - <SheetPrimitive.Close className="absolute cursor-pointer right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"> - <X className="size-5" /> - <span className="sr-only">Close</span> - </SheetPrimitive.Close> - {children} - </SheetPrimitive.Content> - </SheetPortal> + <SheetPortal> + <SheetOverlay /> + <SheetPrimitive.Content + ref={ref} + className={cn(sheetVariants({ side }), className)} + onOpenAutoFocus={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} + {...props} + > + <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 cursor-pointer rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> + <X className="size-5" /> + <span className="sr-only">Close</span> + </SheetPrimitive.Close> + {children} + </SheetPrimitive.Content> + </SheetPortal> )); SheetContent.displayName = SheetPrimitive.Content.displayName; const SheetHeader = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col space-y-2 text-center sm:text-left", - className - )} - {...props} - /> + <div + className={cn( + "flex flex-col space-y-2 text-center sm:text-left", + className, + )} + {...props} + /> ); SheetHeader.displayName = "SheetHeader"; const SheetFooter = ({ - className, - ...props + className, + ...props }: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", - className - )} - {...props} - /> + <div + className={cn( + "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4", + className, + )} + {...props} + /> ); SheetFooter.displayName = "SheetFooter"; const SheetTitle = React.forwardRef< - React.ElementRef<typeof SheetPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> + React.ElementRef<typeof SheetPrimitive.Title>, + React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> >(({ className, ...props }, ref) => ( - <SheetPrimitive.Title - ref={ref} - className={cn("text-lg font-semibold text-foreground", className)} - {...props} - /> + <SheetPrimitive.Title + ref={ref} + className={cn("text-foreground text-lg font-semibold", className)} + {...props} + /> )); SheetTitle.displayName = SheetPrimitive.Title.displayName; const SheetDescription = React.forwardRef< - React.ElementRef<typeof SheetPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> + React.ElementRef<typeof SheetPrimitive.Description>, + React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> >(({ className, ...props }, ref) => ( - <SheetPrimitive.Description - ref={ref} - className={cn("text-sm text-muted-foreground", className)} - {...props} - /> + <SheetPrimitive.Description + ref={ref} + className={cn("text-muted-foreground text-sm", className)} + {...props} + /> )); SheetDescription.displayName = SheetPrimitive.Description.displayName; export { - Sheet, - SheetPortal, - SheetOverlay, - SheetTrigger, - SheetClose, - SheetContent, - SheetHeader, - SheetFooter, - SheetTitle, - SheetDescription, + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, }; diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index 819116c3..3ac9d93a 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -1,763 +1,725 @@ "use client"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { PanelLeftIcon } from "lucide-react"; import * as React from "react"; -import { Slot as SlotPrimitive } from "radix-ui"; -import { VariantProps, cva } from "class-variance-authority"; -import { PanelLeft } from "lucide-react"; - -import { useIsMobile } from "../../hooks/use-mobile"; -import { cn } from "../../lib/utils"; -import { Button } from "./button"; -import { Input } from "./input"; -import { Separator } from "./separator"; -import { Sheet, SheetContent, SheetTitle } from "./sheet"; -import { Skeleton } from "./skeleton"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { Skeleton } from "@/components/ui/skeleton"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "./tooltip"; + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useIsMobile } from "@/hooks/use-mobile"; +import { cn } from "@/utils/ui"; -const SIDEBAR_COOKIE_NAME = "sidebar:state"; +const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_WIDTH = "16rem"; const SIDEBAR_WIDTH_MOBILE = "18rem"; const SIDEBAR_WIDTH_ICON = "3rem"; const SIDEBAR_KEYBOARD_SHORTCUT = "b"; -type SidebarContext = { - state: "expanded" | "collapsed"; - open: boolean; - setOpen: (open: boolean) => void; - openMobile: boolean; - setOpenMobile: (open: boolean) => void; - isMobile: boolean; - toggleSidebar: () => void; +type SidebarContextProps = { + state: "expanded" | "collapsed"; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; }; -const SidebarContext = React.createContext<SidebarContext | null>(null); +const SidebarContext = React.createContext<SidebarContextProps | null>(null); function useSidebar() { - const context = React.useContext(SidebarContext); - if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider."); - } + const context = React.useContext(SidebarContext); + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider."); + } - return context; + return context; } -const SidebarProvider = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - defaultOpen?: boolean; - open?: boolean; - onOpenChange?: (open: boolean) => void; - } ->( - ( - { - defaultOpen = true, - open: openProp, - onOpenChange: setOpenProp, - className, - style, - children, - ...props - }, - ref - ) => { - const isMobile = useIsMobile(); - const [openMobile, setOpenMobile] = React.useState(false); +function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: React.ComponentProps<"div"> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}) { + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = React.useState(false); - // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen); - const open = openProp ?? _open; - const setOpen = React.useCallback( - (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === "function" ? value(open) : value; - if (setOpenProp) { - setOpenProp(openState); - } else { - _setOpen(openState); - } + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value; + if (setOpenProp) { + setOpenProp(openState); + } else { + _setOpen(openState); + } - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; - }, - [setOpenProp, open] - ); + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + }, + [setOpenProp, open], + ); - // Helper to toggle the sidebar. - const toggleSidebar = React.useCallback(() => { - return isMobile - ? setOpenMobile((open) => !open) - : setOpen((open) => !open); - }, [isMobile, setOpen, setOpenMobile]); + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); + }, [isMobile, setOpen, setOpenMobile]); - // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault(); - toggleSidebar(); - } - }; + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + toggleSidebar(); + } + }; - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [toggleSidebar]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [toggleSidebar]); - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? "expanded" : "collapsed"; + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed"; - const contextValue = React.useMemo<SidebarContext>( - () => ({ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - }), - [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] - ); + const contextValue = React.useMemo<SidebarContextProps>( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], + ); - return ( - <SidebarContext.Provider value={contextValue}> - <TooltipProvider delayDuration={0}> - <div - style={ - { - "--sidebar-width": SIDEBAR_WIDTH, - "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, - ...style, - } as React.CSSProperties - } - className={cn( - "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar", - className - )} - ref={ref} - {...props} - > - {children} - </div> - </TooltipProvider> - </SidebarContext.Provider> - ); - } -); -SidebarProvider.displayName = "SidebarProvider"; + return ( + <SidebarContext.Provider value={contextValue}> + <TooltipProvider delayDuration={0}> + <div + data-slot="sidebar-wrapper" + style={ + { + "--sidebar-width": SIDEBAR_WIDTH, + "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, + ...style, + } as React.CSSProperties + } + className={cn( + "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", + className, + )} + {...props} + > + {children} + </div> + </TooltipProvider> + </SidebarContext.Provider> + ); +} -const Sidebar = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - side?: "left" | "right"; - variant?: "sidebar" | "floating" | "inset"; - collapsible?: "offcanvas" | "icon" | "none"; - } ->( - ( - { - side = "left", - variant = "sidebar", - collapsible = "offcanvas", - className, - children, - ...props - }, - ref - ) => { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); +function Sidebar({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props +}: React.ComponentProps<"div"> & { + side?: "left" | "right"; + variant?: "sidebar" | "floating" | "inset"; + collapsible?: "offcanvas" | "icon" | "none"; +}) { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); - if (collapsible === "none") { - return ( - <div - className={cn( - "flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground", - className - )} - ref={ref} - {...props} - > - {children} - </div> - ); - } + if (collapsible === "none") { + return ( + <div + data-slot="sidebar" + className={cn( + "bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", + className, + )} + {...props} + > + {children} + </div> + ); + } - if (isMobile) { - return ( - <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> - <SheetContent - data-sidebar="sidebar" - data-mobile="true" - className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden" - style={ - { - "--sidebar-width": SIDEBAR_WIDTH_MOBILE, - } as React.CSSProperties - } - side={side} - > - <SheetTitle className="sr-only">Navigation Menu</SheetTitle> - <div className="flex h-full w-full flex-col">{children}</div> - </SheetContent> - </Sheet> - ); - } + if (isMobile) { + return ( + <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}> + <SheetContent + data-sidebar="sidebar" + data-slot="sidebar" + data-mobile="true" + className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" + style={ + { + "--sidebar-width": SIDEBAR_WIDTH_MOBILE, + } as React.CSSProperties + } + side={side} + > + <SheetHeader className="sr-only"> + <SheetTitle>Sidebar</SheetTitle> + <SheetDescription>Displays the mobile sidebar.</SheetDescription> + </SheetHeader> + <div className="flex size-full flex-col">{children}</div> + </SheetContent> + </Sheet> + ); + } - return ( - <div - ref={ref} - className="group peer hidden md:block text-sidebar-foreground" - data-state={state} - data-collapsible={state === "collapsed" ? collapsible : ""} - data-variant={variant} - data-side={side} - > - {/* This is what handles the sidebar gap on desktop */} - <div - className={cn( - "duration-200 relative h-full w-(--sidebar-width) bg-transparent transition-[width] ease-linear", - "group-data-[collapsible=offcanvas]:w-0", - "group-data-[side=right]:rotate-180", - variant === "floating" || variant === "inset" - ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" - : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)" - )} - /> - <div - className={cn( - "duration-200 fixed h-[calc(100vh-4rem)] top-16 z-10 hidden w-(--sidebar-width) transition-[left,right,width] ease-linear md:flex", - side === "left" - ? "left-0 border-r group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" - : "right-0 border-l group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", - variant === "floating" || variant === "inset" - ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" - : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)", - className - )} - {...props} - > - <div - data-sidebar="sidebar" - className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm" - > - {children} - </div> - </div> - </div> - ); - } -); -Sidebar.displayName = "Sidebar"; + return ( + <div + className="group peer text-sidebar-foreground hidden md:block" + data-state={state} + data-collapsible={state === "collapsed" ? collapsible : ""} + data-variant={variant} + data-side={side} + data-slot="sidebar" + > + {/* This is what handles the sidebar gap on desktop */} + <div + data-slot="sidebar-gap" + className={cn( + "relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear", + "group-data-[collapsible=offcanvas]:w-0", + "group-data-[side=right]:rotate-180", + variant === "floating" || variant === "inset" + ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" + : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)", + )} + /> + <div + data-slot="sidebar-container" + className={cn( + "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex", + side === "left" + ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]" + : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]", + // Adjust the padding for floating and inset variants. + variant === "floating" || variant === "inset" + ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" + : "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l", + className, + )} + {...props} + > + <div + data-sidebar="sidebar" + data-slot="sidebar-inner" + className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex size-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm" + > + {children} + </div> + </div> + </div> + ); +} -const SidebarTrigger = React.forwardRef< - React.ElementRef<typeof Button>, - React.ComponentProps<typeof Button> ->(({ className, onClick, ...props }, ref) => { - const { toggleSidebar } = useSidebar(); +function SidebarTrigger({ + className, + onClick, + ...props +}: React.ComponentProps<typeof Button>) { + const { toggleSidebar } = useSidebar(); - return ( - <Button - ref={ref} - data-sidebar="trigger" - variant="text" - size="icon" - className={cn("h-8 w-8", className)} - onClick={(event) => { - onClick?.(event); - toggleSidebar(); - }} - {...props} - > - <PanelLeft /> - <span className="sr-only">Toggle Sidebar</span> - </Button> - ); -}); -SidebarTrigger.displayName = "SidebarTrigger"; + return ( + <Button + data-sidebar="trigger" + data-slot="sidebar-trigger" + variant="ghost" + size="icon" + className={cn("size-7", className)} + onClick={(event) => { + onClick?.(event); + toggleSidebar(); + }} + {...props} + > + <PanelLeftIcon /> + <span className="sr-only">Toggle Sidebar</span> + </Button> + ); +} -const SidebarRail = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> ->(({ className, ...props }, ref) => { - const { toggleSidebar } = useSidebar(); +function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar(); - return ( - <button - ref={ref} - data-sidebar="rail" - aria-label="Toggle Sidebar" - tabIndex={-1} - onClick={toggleSidebar} - title="Toggle Sidebar" - className={cn( - "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex", - "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", - "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", - "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar", - "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", - "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", - className - )} - {...props} - /> - ); -}); -SidebarRail.displayName = "SidebarRail"; + return ( + <button + data-sidebar="rail" + data-slot="sidebar-rail" + aria-label="Toggle Sidebar" + tabIndex={-1} + onClick={toggleSidebar} + title="Toggle Sidebar" + className={cn( + "hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex", + "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", + "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", + "hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full", + "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", + "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", + className, + )} + {...props} + /> + ); +} -const SidebarInset = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"main"> ->(({ className, ...props }, ref) => { - return ( - <main - ref={ref} - className={cn( - "relative flex min-h-svh flex-1 flex-col bg-background", - "peer-data-[variant=inset]:min-h-[calc(100svh-(--spacing(4)))] md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm", - className - )} - {...props} - /> - ); -}); -SidebarInset.displayName = "SidebarInset"; +function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { + return ( + <main + data-slot="sidebar-inset" + className={cn( + "bg-background relative flex w-full flex-1 flex-col", + "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", + className, + )} + {...props} + /> + ); +} -const SidebarInput = React.forwardRef< - React.ElementRef<typeof Input>, - React.ComponentProps<typeof Input> ->(({ className, ...props }, ref) => { - return ( - <Input - ref={ref} - data-sidebar="input" - className={cn( - "h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring", - className - )} - {...props} - /> - ); -}); -SidebarInput.displayName = "SidebarInput"; +function SidebarInput({ + className, + ...props +}: React.ComponentProps<typeof Input>) { + return ( + <Input + data-slot="sidebar-input" + data-sidebar="input" + className={cn("bg-background h-8 w-full shadow-none", className)} + {...props} + /> + ); +} -const SidebarHeader = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( - <div - ref={ref} - data-sidebar="header" - className={cn("flex flex-col gap-2 p-2", className)} - {...props} - /> - ); -}); -SidebarHeader.displayName = "SidebarHeader"; +function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sidebar-header" + data-sidebar="header" + className={cn("flex flex-col gap-2 p-2", className)} + {...props} + /> + ); +} -const SidebarFooter = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( - <div - ref={ref} - data-sidebar="footer" - className={cn("flex flex-col gap-2 p-2", className)} - {...props} - /> - ); -}); -SidebarFooter.displayName = "SidebarFooter"; +function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sidebar-footer" + data-sidebar="footer" + className={cn("flex flex-col gap-2 p-2", className)} + {...props} + /> + ); +} -const SidebarSeparator = React.forwardRef< - React.ElementRef<typeof Separator>, - React.ComponentProps<typeof Separator> ->(({ className, ...props }, ref) => { - return ( - <Separator - ref={ref} - data-sidebar="separator" - className={cn("mx-2 w-auto bg-sidebar-border", className)} - {...props} - /> - ); -}); -SidebarSeparator.displayName = "SidebarSeparator"; +function SidebarSeparator({ + className, + ...props +}: React.ComponentProps<typeof Separator>) { + return ( + <Separator + data-slot="sidebar-separator" + data-sidebar="separator" + className={cn("bg-sidebar-border mx-2 w-auto", className)} + {...props} + /> + ); +} -const SidebarContent = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( - <div - ref={ref} - data-sidebar="content" - className={cn( - "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", - className - )} - {...props} - /> - ); -}); -SidebarContent.displayName = "SidebarContent"; +function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sidebar-content" + data-sidebar="content" + className={cn( + "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", + className, + )} + {...props} + /> + ); +} -const SidebarGroup = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => { - return ( - <div - ref={ref} - data-sidebar="group" - className={cn("relative flex w-full min-w-0 flex-col p-2", className)} - {...props} - /> - ); -}); -SidebarGroup.displayName = "SidebarGroup"; +function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( + <div + data-slot="sidebar-group" + data-sidebar="group" + className={cn("relative flex w-full min-w-0 flex-col p-2", className)} + {...props} + /> + ); +} -const SidebarGroupLabel = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { asChild?: boolean } ->(({ className, asChild = false, ...props }, ref) => { - const Comp = asChild ? SlotPrimitive.Slot : "div"; +function SidebarGroupLabel({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { asChild?: boolean }) { + const Comp = asChild ? Slot : "div"; - return ( - <Comp - ref={ref as any} - data-sidebar="group-label" - className={cn( - "duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-hidden ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", - "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", - className - )} - {...props} - /> - ); -}); -SidebarGroupLabel.displayName = "SidebarGroupLabel"; + return ( + <Comp + data-slot="sidebar-group-label" + data-sidebar="group-label" + className={cn( + "text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", + className, + )} + {...props} + /> + ); +} -const SidebarGroupAction = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & { asChild?: boolean } ->(({ className, asChild = false, ...props }, ref) => { - const Comp = asChild ? SlotPrimitive.Slot : "button"; +function SidebarGroupAction({ + className, + asChild = false, + ...props +}: React.ComponentProps<"button"> & { asChild?: boolean }) { + const Comp = asChild ? Slot : "button"; - return ( - <Comp - ref={ref as any} - data-sidebar="group-action" - className={cn( - "absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", - // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 md:after:hidden", - "group-data-[collapsible=icon]:hidden", - className - )} - {...props} - /> - ); -}); -SidebarGroupAction.displayName = "SidebarGroupAction"; + return ( + <Comp + data-slot="sidebar-group-action" + data-sidebar="group-action" + className={cn( + "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 md:after:hidden", + "group-data-[collapsible=icon]:hidden", + className, + )} + {...props} + /> + ); +} -const SidebarGroupContent = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - data-sidebar="group-content" - className={cn("w-full text-sm", className)} - {...props} - /> -)); -SidebarGroupContent.displayName = "SidebarGroupContent"; +function SidebarGroupContent({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( + <div + data-slot="sidebar-group-content" + data-sidebar="group-content" + className={cn("w-full text-sm", className)} + {...props} + /> + ); +} -const SidebarMenu = React.forwardRef< - HTMLUListElement, - React.ComponentProps<"ul"> ->(({ className, ...props }, ref) => ( - <ul - ref={ref} - data-sidebar="menu" - className={cn("flex w-full min-w-0 flex-col gap-1", className)} - {...props} - /> -)); -SidebarMenu.displayName = "SidebarMenu"; +function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { + return ( + <ul + data-slot="sidebar-menu" + data-sidebar="menu" + className={cn("flex w-full min-w-0 flex-col gap-1", className)} + {...props} + /> + ); +} -const SidebarMenuItem = React.forwardRef< - HTMLLIElement, - React.ComponentProps<"li"> ->(({ className, ...props }, ref) => ( - <li - ref={ref} - data-sidebar="menu-item" - className={cn("group/menu-item relative", className)} - {...props} - /> -)); -SidebarMenuItem.displayName = "SidebarMenuItem"; +function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { + return ( + <li + data-slot="sidebar-menu-item" + data-sidebar="menu-item" + className={cn("group/menu-item relative", className)} + {...props} + /> + ); +} const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", - { - variants: { - variant: { - default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", - outline: - "bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]", - }, - size: { - default: "h-8 text-sm", - sm: "h-7 text-xs", - lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + { + variants: { + variant: { + default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", + outline: + "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", + }, + size: { + default: "h-8 text-sm", + sm: "h-7 text-xs", + lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, ); -const SidebarMenuButton = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & { - asChild?: boolean; - isActive?: boolean; - tooltip?: string | React.ComponentProps<typeof TooltipContent>; - } & VariantProps<typeof sidebarMenuButtonVariants> ->( - ( - { - asChild = false, - isActive = false, - variant = "default", - size = "default", - tooltip, - className, - ...props - }, - ref - ) => { - const Comp = asChild ? SlotPrimitive.Slot : "button"; - const { isMobile, state } = useSidebar(); +function SidebarMenuButton({ + asChild = false, + isActive = false, + variant = "default", + size = "default", + tooltip, + className, + ...props +}: React.ComponentProps<"button"> & { + asChild?: boolean; + isActive?: boolean; + tooltip?: string | React.ComponentProps<typeof TooltipContent>; +} & VariantProps<typeof sidebarMenuButtonVariants>) { + const Comp = asChild ? Slot : "button"; + const { isMobile, state } = useSidebar(); - const button = ( - <Comp - ref={ref as any} - data-sidebar="menu-button" - data-size={size} - data-active={isActive} - className={cn(sidebarMenuButtonVariants({ variant, size }), className)} - {...props} - /> - ); + const button = ( + <Comp + data-slot="sidebar-menu-button" + data-sidebar="menu-button" + data-size={size} + data-active={isActive} + className={cn(sidebarMenuButtonVariants({ variant, size }), className)} + {...props} + /> + ); - if (!tooltip) { - return button; - } + if (!tooltip) { + return button; + } - if (typeof tooltip === "string") { - tooltip = { - children: tooltip, - }; - } + if (typeof tooltip === "string") { + tooltip = { + children: tooltip, + }; + } - return ( - <Tooltip> - <TooltipTrigger asChild>{button}</TooltipTrigger> - <TooltipContent - side="right" - align="center" - hidden={state !== "collapsed" || isMobile} - {...tooltip} - /> - </Tooltip> - ); - } -); -SidebarMenuButton.displayName = "SidebarMenuButton"; + return ( + <Tooltip> + <TooltipTrigger asChild>{button}</TooltipTrigger> + <TooltipContent + side="right" + align="center" + hidden={state !== "collapsed" || isMobile} + {...tooltip} + /> + </Tooltip> + ); +} -const SidebarMenuAction = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> & { - asChild?: boolean; - showOnHover?: boolean; - } ->(({ className, asChild = false, showOnHover = false, ...props }, ref) => { - const Comp = asChild ? SlotPrimitive.Slot : "button"; +function SidebarMenuAction({ + className, + asChild = false, + showOnHover = false, + ...props +}: React.ComponentProps<"button"> & { + asChild?: boolean; + showOnHover?: boolean; +}) { + const Comp = asChild ? Slot : "button"; - return ( - <Comp - ref={ref as any} - data-sidebar="menu-action" - className={cn( - "absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0", - // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 md:after:hidden", - "peer-data-[size=sm]/menu-button:top-1", - "peer-data-[size=default]/menu-button:top-1.5", - "peer-data-[size=lg]/menu-button:top-2.5", - "group-data-[collapsible=icon]:hidden", - showOnHover && - "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0", - className - )} - {...props} - /> - ); -}); -SidebarMenuAction.displayName = "SidebarMenuAction"; + return ( + <Comp + data-slot="sidebar-menu-action" + data-sidebar="menu-action" + className={cn( + "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 md:after:hidden", + "peer-data-[size=sm]/menu-button:top-1", + "peer-data-[size=default]/menu-button:top-1.5", + "peer-data-[size=lg]/menu-button:top-2.5", + "group-data-[collapsible=icon]:hidden", + showOnHover && + "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", + className, + )} + {...props} + /> + ); +} -const SidebarMenuBadge = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - data-sidebar="menu-badge" - className={cn( - "absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none", - "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", - "peer-data-[size=sm]/menu-button:top-1", - "peer-data-[size=default]/menu-button:top-1.5", - "peer-data-[size=lg]/menu-button:top-2.5", - "group-data-[collapsible=icon]:hidden", - className - )} - {...props} - /> -)); -SidebarMenuBadge.displayName = "SidebarMenuBadge"; +function SidebarMenuBadge({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( + <div + data-slot="sidebar-menu-badge" + data-sidebar="menu-badge" + className={cn( + "text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none", + "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground", + "peer-data-[size=sm]/menu-button:top-1", + "peer-data-[size=default]/menu-button:top-1.5", + "peer-data-[size=lg]/menu-button:top-2.5", + "group-data-[collapsible=icon]:hidden", + className, + )} + {...props} + /> + ); +} -const SidebarMenuSkeleton = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - showIcon?: boolean; - } ->(({ className, showIcon = false, ...props }, ref) => { - // Random width between 50 to 90%. - const width = React.useMemo(() => { - return `${Math.floor(Math.random() * 40) + 50}%`; - }, []); +function SidebarMenuSkeleton({ + className, + showIcon = false, + ...props +}: React.ComponentProps<"div"> & { + showIcon?: boolean; +}) { + // Random width between 50 to 90%. + const width = React.useMemo(() => { + return `${Math.floor(Math.random() * 40) + 50}%`; + }, []); - return ( - <div - ref={ref} - data-sidebar="menu-skeleton" - className={cn("rounded-md h-8 flex gap-2 px-2 items-center", className)} - {...props} - > - {showIcon && ( - <Skeleton - className="size-4 rounded-md" - data-sidebar="menu-skeleton-icon" - /> - )} - <Skeleton - className="h-4 flex-1 max-w-(--skeleton-width)" - data-sidebar="menu-skeleton-text" - style={ - { - "--skeleton-width": width, - } as React.CSSProperties - } - /> - </div> - ); -}); -SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"; + return ( + <div + data-slot="sidebar-menu-skeleton" + data-sidebar="menu-skeleton" + className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)} + {...props} + > + {showIcon && ( + <Skeleton + className="size-4 rounded-md" + data-sidebar="menu-skeleton-icon" + /> + )} + <Skeleton + className="h-4 max-w-(--skeleton-width) flex-1" + data-sidebar="menu-skeleton-text" + style={ + { + "--skeleton-width": width, + } as React.CSSProperties + } + /> + </div> + ); +} -const SidebarMenuSub = React.forwardRef< - HTMLUListElement, - React.ComponentProps<"ul"> ->(({ className, ...props }, ref) => ( - <ul - ref={ref} - data-sidebar="menu-sub" - className={cn( - "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5", - "group-data-[collapsible=icon]:hidden", - className - )} - {...props} - /> -)); -SidebarMenuSub.displayName = "SidebarMenuSub"; +function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { + return ( + <ul + data-slot="sidebar-menu-sub" + data-sidebar="menu-sub" + className={cn( + "border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5", + "group-data-[collapsible=icon]:hidden", + className, + )} + {...props} + /> + ); +} -const SidebarMenuSubItem = React.forwardRef< - HTMLLIElement, - React.ComponentProps<"li"> ->(({ ...props }, ref) => <li ref={ref} {...props} />); -SidebarMenuSubItem.displayName = "SidebarMenuSubItem"; +function SidebarMenuSubItem({ + className, + ...props +}: React.ComponentProps<"li">) { + return ( + <li + data-slot="sidebar-menu-sub-item" + data-sidebar="menu-sub-item" + className={cn("group/menu-sub-item relative", className)} + {...props} + /> + ); +} -const SidebarMenuSubButton = React.forwardRef< - HTMLAnchorElement, - React.ComponentProps<"a"> & { - asChild?: boolean; - size?: "sm" | "md"; - isActive?: boolean; - } ->(({ asChild = false, size = "md", isActive, className, ...props }, ref) => { - const Comp = asChild ? SlotPrimitive.Slot : "a"; +function SidebarMenuSubButton({ + asChild = false, + size = "md", + isActive = false, + className, + ...props +}: React.ComponentProps<"a"> & { + asChild?: boolean; + size?: "sm" | "md"; + isActive?: boolean; +}) { + const Comp = asChild ? Slot : "a"; - return ( - <Comp - ref={ref as any} - data-sidebar="menu-sub-button" - data-size={size} - data-active={isActive} - className={cn( - "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", - "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", - size === "sm" && "text-xs", - size === "md" && "text-sm", - "group-data-[collapsible=icon]:hidden", - className - )} - {...props} - /> - ); -}); -SidebarMenuSubButton.displayName = "SidebarMenuSubButton"; + return ( + <Comp + data-slot="sidebar-menu-sub-button" + data-sidebar="menu-sub-button" + data-size={size} + data-active={isActive} + className={cn( + "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", + size === "sm" && "text-xs", + size === "md" && "text-sm", + "group-data-[collapsible=icon]:hidden", + className, + )} + {...props} + /> + ); +} export { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupAction, - SidebarGroupContent, - SidebarGroupLabel, - SidebarHeader, - SidebarInput, - SidebarInset, - SidebarMenu, - SidebarMenuAction, - SidebarMenuBadge, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSkeleton, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, - SidebarProvider, - SidebarRail, - SidebarSeparator, - SidebarTrigger, - useSidebar, + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupAction, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarInput, + SidebarInset, + SidebarMenu, + SidebarMenuAction, + SidebarMenuBadge, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSkeleton, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarProvider, + SidebarRail, + SidebarSeparator, + SidebarTrigger, + useSidebar, }; diff --git a/apps/web/src/components/ui/skeleton.tsx b/apps/web/src/components/ui/skeleton.tsx index 4525c3a0..bf59fec2 100644 --- a/apps/web/src/components/ui/skeleton.tsx +++ b/apps/web/src/components/ui/skeleton.tsx @@ -1,15 +1,15 @@ -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; function Skeleton({ - className, - ...props + className, + ...props }: React.HTMLAttributes<HTMLDivElement>) { - return ( - <div - className={cn("animate-pulse rounded-md bg-primary/10", className)} - {...props} - /> - ); + return ( + <div + className={cn("bg-primary/10 animate-pulse rounded-md", className)} + {...props} + /> + ); } export { Skeleton }; diff --git a/apps/web/src/components/ui/slider.tsx b/apps/web/src/components/ui/slider.tsx index d4afdd06..994159a1 100644 --- a/apps/web/src/components/ui/slider.tsx +++ b/apps/web/src/components/ui/slider.tsx @@ -3,25 +3,25 @@ import * as React from "react"; import { Slider as SliderPrimitive } from "radix-ui"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Slider = React.forwardRef< - React.ElementRef<typeof SliderPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> + React.ElementRef<typeof SliderPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> >(({ className, ...props }, ref) => ( - <SliderPrimitive.Root - ref={ref} - className={cn( - "relative flex w-full touch-none select-none items-center", - className - )} - {...props} - > - <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20"> - <SliderPrimitive.Range className="absolute h-full bg-primary" /> - </SliderPrimitive.Track> - <SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow-sm transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" /> - </SliderPrimitive.Root> + <SliderPrimitive.Root + ref={ref} + className={cn( + "relative flex w-full touch-none items-center select-none", + className, + )} + {...props} + > + <SliderPrimitive.Track className="bg-primary/20 relative h-1.5 w-full grow overflow-hidden rounded-full"> + <SliderPrimitive.Range className="bg-primary absolute h-full" /> + </SliderPrimitive.Track> + <SliderPrimitive.Thumb className="border-primary/50 bg-background focus-visible:ring-ring block size-4 rounded-full border shadow-sm focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" /> + </SliderPrimitive.Root> )); Slider.displayName = SliderPrimitive.Root.displayName; diff --git a/apps/web/src/components/ui/sonner.tsx b/apps/web/src/components/ui/sonner.tsx index 13694b43..4bae2fad 100644 --- a/apps/web/src/components/ui/sonner.tsx +++ b/apps/web/src/components/ui/sonner.tsx @@ -6,30 +6,30 @@ import { Toaster as Sonner } from "sonner"; type ToasterProps = React.ComponentProps<typeof Sonner>; const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme(); + const { theme = "system" } = useTheme(); - return ( - <Sonner - theme={theme as ToasterProps["theme"]} - className="toaster group" - position="top-center" - offset={20} - toastOptions={{ - classNames: { - toast: - "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", - description: "group-[.toast]:text-muted-foreground", - actionButton: - "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", - cancelButton: - "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", - }, - }} - expand={false} - richColors - {...props} - /> - ); + return ( + <Sonner + theme={theme as ToasterProps["theme"]} + className="toaster group" + position="top-center" + offset={20} + toastOptions={{ + classNames: { + toast: + "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", + description: "group-[.toast]:text-muted-foreground", + actionButton: + "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", + cancelButton: + "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", + }, + }} + expand={false} + richColors + {...props} + /> + ); }; export { Toaster }; diff --git a/apps/web/src/components/ui/spinner.tsx b/apps/web/src/components/ui/spinner.tsx new file mode 100644 index 00000000..a46792f7 --- /dev/null +++ b/apps/web/src/components/ui/spinner.tsx @@ -0,0 +1,17 @@ +import { HugeiconsIcon, type HugeiconsIconProps } from "@hugeicons/react"; +import { Loading03Icon } from "@hugeicons/core-free-icons"; +import { cn } from "@/utils/ui"; + +function Spinner({ className, ...props }: Omit<HugeiconsIconProps, "icon">) { + return ( + <HugeiconsIcon + icon={Loading03Icon} + role="status" + aria-label="Loading" + className={cn("size-4 animate-spin", className)} + {...props} + /> + ) +} + +export { Spinner } diff --git a/apps/web/src/components/ui/split-button.tsx b/apps/web/src/components/ui/split-button.tsx index ca3b518c..26e54cab 100644 --- a/apps/web/src/components/ui/split-button.tsx +++ b/apps/web/src/components/ui/split-button.tsx @@ -1,89 +1,89 @@ -import { Button, ButtonProps } from "@/components/ui/button";
+import { Button, type ButtonProps } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
-import { ReactNode, forwardRef } from "react";
-import { cn } from "@/lib/utils";
+import { type ReactNode, forwardRef } from "react";
+import { cn } from "@/utils/ui";
interface SplitButtonProps {
- children: ReactNode;
- className?: string;
+ children: ReactNode;
+ className?: string;
}
interface SplitButtonSideProps extends Omit<ButtonProps, "variant" | "size"> {
- children: ReactNode;
+ children: ReactNode;
}
const SplitButton = forwardRef<HTMLDivElement, SplitButtonProps>(
- ({ children, className, ...props }, ref) => {
- return (
- <div
- ref={ref}
- className={cn(
- "inline-flex rounded-full h-7 border border-input bg-panel-accent overflow-hidden",
- className
- )}
- {...props}
- >
- {children}
- </div>
- );
- }
+ ({ children, className, ...props }, ref) => {
+ return (
+ <div
+ ref={ref}
+ className={cn(
+ "border-input bg-panel-accent inline-flex h-7 overflow-hidden rounded-lg border",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+ </div>
+ );
+ },
);
SplitButton.displayName = "SplitButton";
const SplitButtonSide = forwardRef<
- HTMLButtonElement,
- SplitButtonSideProps & { paddingClass: string }
+ HTMLButtonElement,
+ SplitButtonSideProps & { paddingClass: string }
>(({ children, className, paddingClass, onClick, ...props }, ref) => {
- return (
- <Button
- ref={ref}
- variant="text"
- className={cn(
- "h-full rounded-none bg-panel-accent !opacity-100 border-0 gap-0 font-normal transition-colors disabled:text-muted-foreground",
- onClick
- ? "hover:bg-foreground/10 hover:opacity-100 cursor-pointer"
- : "cursor-default select-text",
- paddingClass,
- className
- )}
- onClick={onClick}
- {...props}
- >
- {typeof children === "string" ? (
- <span className="font-normal cursor-text">{children}</span>
- ) : (
- children
- )}
- </Button>
- );
+ return (
+ <Button
+ ref={ref}
+ variant="text"
+ className={cn(
+ "bg-panel-accent disabled:text-muted-foreground h-full gap-0 rounded-none border-0 font-normal !opacity-100",
+ onClick
+ ? "hover:bg-foreground/10 cursor-pointer hover:opacity-100"
+ : "cursor-default select-text",
+ paddingClass,
+ className,
+ )}
+ onClick={onClick}
+ {...props}
+ >
+ {typeof children === "string" ? (
+ <span className="cursor-text font-normal">{children}</span>
+ ) : (
+ children
+ )}
+ </Button>
+ );
});
SplitButtonSide.displayName = "SplitButtonSide";
const SplitButtonLeft = forwardRef<HTMLButtonElement, SplitButtonSideProps>(
- ({ ...props }, ref) => {
- return <SplitButtonSide ref={ref} paddingClass="pl-3 pr-2" {...props} />;
- }
+ ({ ...props }, ref) => {
+ return <SplitButtonSide ref={ref} paddingClass="pl-3 pr-2" {...props} />;
+ },
);
SplitButtonLeft.displayName = "SplitButtonLeft";
const SplitButtonRight = forwardRef<HTMLButtonElement, SplitButtonSideProps>(
- ({ ...props }, ref) => {
- return <SplitButtonSide ref={ref} paddingClass="pl-2 pr-3" {...props} />;
- }
+ ({ ...props }, ref) => {
+ return <SplitButtonSide ref={ref} paddingClass="pl-2 pr-3" {...props} />;
+ },
);
SplitButtonRight.displayName = "SplitButtonRight";
const SplitButtonSeparator = forwardRef<HTMLDivElement, { className?: string }>(
- ({ className, ...props }, ref) => {
- return (
- <Separator
- ref={ref}
- orientation="vertical"
- className={cn("h-full bg-foreground/15", className)}
- {...props}
- />
- );
- }
+ ({ className, ...props }, ref) => {
+ return (
+ <Separator
+ ref={ref}
+ orientation="vertical"
+ className={cn("bg-foreground/15 h-full", className)}
+ {...props}
+ />
+ );
+ },
);
SplitButtonSeparator.displayName = "SplitButtonSeparator";
diff --git a/apps/web/src/components/ui/sponsor-button.tsx b/apps/web/src/components/ui/sponsor-button.tsx deleted file mode 100644 index 146566a6..00000000 --- a/apps/web/src/components/ui/sponsor-button.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { motion } from "motion/react"; -import { ComponentType } from "react"; - -interface SponsorButtonProps { - href: string; - logo: ComponentType<{ className?: string }>; - companyName: string; - className?: string; -} - -export function SponsorButton({ - href, - logo: Logo, - companyName, - className = "", -}: SponsorButtonProps) { - return ( - <motion.a - href={href} - target="_blank" - rel="noopener noreferrer" - className={`inline-flex items-center gap-2 px-3 py-2 rounded-md border border-border bg-background/5 backdrop-blur-xs hover:bg-background/10 transition-all duration-200 group shadow-lg ${className}`} - > - <span className="text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors"> - Sponsored by - </span> - - <div className="flex items-center gap-1.5"> - <div className="text-foreground/90 group-hover:text-foreground transition-colors"> - <Logo className="w-4 h-4" /> - </div> - - <span className="text-xs font-medium text-foreground/90 group-hover:text-foreground transition-colors"> - {companyName} - </span> - </div> - </motion.a> - ); -} diff --git a/apps/web/src/components/ui/switch.tsx b/apps/web/src/components/ui/switch.tsx index f210afdf..c0207804 100644 --- a/apps/web/src/components/ui/switch.tsx +++ b/apps/web/src/components/ui/switch.tsx @@ -3,26 +3,26 @@ import * as React from "react"; import { Switch as SwitchPrimitives } from "radix-ui"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Switch = React.forwardRef< - React.ElementRef<typeof SwitchPrimitives.Root>, - React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> + React.ElementRef<typeof SwitchPrimitives.Root>, + React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> >(({ className, ...props }, ref) => ( - <SwitchPrimitives.Root - className={cn( - "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", - className - )} - {...props} - ref={ref} - > - <SwitchPrimitives.Thumb - className={cn( - "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0" - )} - /> - </SwitchPrimitives.Root> + <SwitchPrimitives.Root + className={cn( + "peer focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", + className, + )} + {...props} + ref={ref} + > + <SwitchPrimitives.Thumb + className={cn( + "bg-background pointer-events-none block size-4 rounded-full shadow-lg ring-0 data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0", + )} + /> + </SwitchPrimitives.Root> )); Switch.displayName = SwitchPrimitives.Root.displayName; diff --git a/apps/web/src/components/ui/table.tsx b/apps/web/src/components/ui/table.tsx index 7481cabb..7e8541ce 100644 --- a/apps/web/src/components/ui/table.tsx +++ b/apps/web/src/components/ui/table.tsx @@ -1,120 +1,120 @@ import * as React from "react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Table = React.forwardRef< - HTMLTableElement, - React.HTMLAttributes<HTMLTableElement> + HTMLTableElement, + React.HTMLAttributes<HTMLTableElement> >(({ className, ...props }, ref) => ( - <div className="relative w-full overflow-auto"> - <table - ref={ref} - className={cn("w-full caption-bottom text-sm", className)} - {...props} - /> - </div> + <div className="relative w-full overflow-auto"> + <table + ref={ref} + className={cn("w-full caption-bottom text-sm", className)} + {...props} + /> + </div> )); Table.displayName = "Table"; const TableHeader = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes<HTMLTableSectionElement> + HTMLTableSectionElement, + React.HTMLAttributes<HTMLTableSectionElement> >(({ className, ...props }, ref) => ( - <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> + <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> )); TableHeader.displayName = "TableHeader"; const TableBody = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes<HTMLTableSectionElement> + HTMLTableSectionElement, + React.HTMLAttributes<HTMLTableSectionElement> >(({ className, ...props }, ref) => ( - <tbody - ref={ref} - className={cn("[&_tr:last-child]:border-0", className)} - {...props} - /> + <tbody + ref={ref} + className={cn("[&_tr:last-child]:border-0", className)} + {...props} + /> )); TableBody.displayName = "TableBody"; const TableFooter = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes<HTMLTableSectionElement> + HTMLTableSectionElement, + React.HTMLAttributes<HTMLTableSectionElement> >(({ className, ...props }, ref) => ( - <tfoot - ref={ref} - className={cn( - "border-t bg-muted/50 font-medium last:[&>tr]:border-b-0", - className - )} - {...props} - /> + <tfoot + ref={ref} + className={cn( + "bg-muted/50 border-t font-medium last:[&>tr]:border-b-0", + className, + )} + {...props} + /> )); TableFooter.displayName = "TableFooter"; const TableRow = React.forwardRef< - HTMLTableRowElement, - React.HTMLAttributes<HTMLTableRowElement> + HTMLTableRowElement, + React.HTMLAttributes<HTMLTableRowElement> >(({ className, ...props }, ref) => ( - <tr - ref={ref} - className={cn( - "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", - className - )} - {...props} - /> + <tr + ref={ref} + className={cn( + "hover:bg-muted/50 data-[state=selected]:bg-muted border-b", + className, + )} + {...props} + /> )); TableRow.displayName = "TableRow"; const TableHead = React.forwardRef< - HTMLTableCellElement, - React.ThHTMLAttributes<HTMLTableCellElement> + HTMLTableCellElement, + React.ThHTMLAttributes<HTMLTableCellElement> >(({ className, ...props }, ref) => ( - <th - ref={ref} - className={cn( - "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", - className - )} - {...props} - /> + <th + ref={ref} + className={cn( + "text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> )); TableHead.displayName = "TableHead"; const TableCell = React.forwardRef< - HTMLTableCellElement, - React.TdHTMLAttributes<HTMLTableCellElement> + HTMLTableCellElement, + React.TdHTMLAttributes<HTMLTableCellElement> >(({ className, ...props }, ref) => ( - <td - ref={ref} - className={cn( - "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", - className - )} - {...props} - /> + <td + ref={ref} + className={cn( + "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", + className, + )} + {...props} + /> )); TableCell.displayName = "TableCell"; const TableCaption = React.forwardRef< - HTMLTableCaptionElement, - React.HTMLAttributes<HTMLTableCaptionElement> + HTMLTableCaptionElement, + React.HTMLAttributes<HTMLTableCaptionElement> >(({ className, ...props }, ref) => ( - <caption - ref={ref} - className={cn("mt-4 text-sm text-muted-foreground", className)} - {...props} - /> + <caption + ref={ref} + className={cn("text-muted-foreground mt-4 text-sm", className)} + {...props} + /> )); TableCaption.displayName = "TableCaption"; export { - Table, - TableHeader, - TableBody, - TableFooter, - TableHead, - TableRow, - TableCell, - TableCaption, + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, }; diff --git a/apps/web/src/components/ui/tabs.tsx b/apps/web/src/components/ui/tabs.tsx index ea628959..3668477c 100644 --- a/apps/web/src/components/ui/tabs.tsx +++ b/apps/web/src/components/ui/tabs.tsx @@ -3,52 +3,52 @@ import * as React from "react"; import { Tabs as TabsPrimitive } from "radix-ui"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Tabs = TabsPrimitive.Root; const TabsList = React.forwardRef< - React.ElementRef<typeof TabsPrimitive.List>, - React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> + React.ElementRef<typeof TabsPrimitive.List>, + React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> >(({ className, ...props }, ref) => ( - <TabsPrimitive.List - ref={ref} - className={cn( - "inline-flex h-auto items-center justify-center gap-2 rounded-lg bg-transparent p-0 text-muted-foreground", - className - )} - {...props} - /> + <TabsPrimitive.List + ref={ref} + className={cn( + "text-muted-foreground inline-flex h-auto items-center justify-center gap-2 rounded-lg bg-transparent p-0", + className, + )} + {...props} + /> )); TabsList.displayName = TabsPrimitive.List.displayName; const TabsTrigger = React.forwardRef< - React.ElementRef<typeof TabsPrimitive.Trigger>, - React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> + React.ElementRef<typeof TabsPrimitive.Trigger>, + React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> >(({ className, ...props }, ref) => ( - <TabsPrimitive.Trigger - ref={ref} - className={cn( - "inline-flex items-center cursor-pointer justify-center whitespace-nowrap rounded-lg px-3 py-1 text-sm font-medium ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-card data-[state=active]:text-foreground", - className - )} - {...props} - /> + <TabsPrimitive.Trigger + ref={ref} + className={cn( + "ring-offset-background focus-visible:ring-ring data-[state=active]:bg-panel-accent data-[state=active]:text-foreground inline-flex cursor-pointer items-center justify-center rounded-lg px-3 py-1 text-sm font-medium whitespace-nowrap focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50", + className, + )} + {...props} + /> )); TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; const TabsContent = React.forwardRef< - React.ElementRef<typeof TabsPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> + React.ElementRef<typeof TabsPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> >(({ className, ...props }, ref) => ( - <TabsPrimitive.Content - ref={ref} - className={cn( - "mt-2 ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", - className - )} - {...props} - /> + <TabsPrimitive.Content + ref={ref} + className={cn( + "ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden", + className, + )} + {...props} + /> )); TabsContent.displayName = TabsPrimitive.Content.displayName; diff --git a/apps/web/src/components/ui/textarea.tsx b/apps/web/src/components/ui/textarea.tsx index d9ae9ea6..9c098475 100644 --- a/apps/web/src/components/ui/textarea.tsx +++ b/apps/web/src/components/ui/textarea.tsx @@ -1,23 +1,23 @@ import * as React from "react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const Textarea = React.forwardRef< - HTMLTextAreaElement, - React.ComponentProps<"textarea"> + HTMLTextAreaElement, + React.ComponentProps<"textarea"> >(({ className, ...props }, ref) => { - return ( - <textarea - className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex min-h-[60px] w-full rounded-md border bg-accent/50 px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]", - "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - className - )} - ref={ref} - {...props} - /> - ); + return ( + <textarea + className={cn( + "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input bg-accent/50 flex min-h-[60px] w-full rounded-md border px-3 py-2 text-base shadow-xs outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "focus-visible:border-primary focus-visible:ring-4 focus-visible:ring-primary/10", + "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + className, + )} + ref={ref} + {...props} + /> + ); }); Textarea.displayName = "Textarea"; diff --git a/apps/web/src/components/ui/toast.tsx b/apps/web/src/components/ui/toast.tsx index bd733f63..44dc790f 100644 --- a/apps/web/src/components/ui/toast.tsx +++ b/apps/web/src/components/ui/toast.tsx @@ -5,110 +5,110 @@ import { Toast as ToastPrimitives } from "radix-ui"; import { cva, type VariantProps } from "class-variance-authority"; import { X } from "lucide-react"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const ToastProvider = ToastPrimitives.Provider; const ToastViewport = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Viewport>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport> + React.ElementRef<typeof ToastPrimitives.Viewport>, + React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport> >(({ className, ...props }, ref) => ( - <ToastPrimitives.Viewport - ref={ref} - className={cn( - "fixed top-0 z-100 flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", - className - )} - {...props} - /> + <ToastPrimitives.Viewport + ref={ref} + className={cn( + "fixed top-0 z-100 flex max-h-screen w-full flex-col-reverse p-4 sm:top-auto sm:right-0 sm:bottom-0 sm:flex-col md:max-w-[420px]", + className, + )} + {...props} + /> )); ToastViewport.displayName = ToastPrimitives.Viewport.displayName; const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full sm:data-[state=open]:slide-in-from-bottom-full", - { - variants: { - variant: { - default: "border bg-background text-foreground", - destructive: - "destructive group border-destructive/35 bg-destructive/15 text-destructive-foreground backdrop-blur-lg", - }, - }, - defaultVariants: { - variant: "default", - }, - } + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full sm:data-[state=open]:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive/35 bg-destructive/15 text-destructive-foreground backdrop-blur-lg", + }, + }, + defaultVariants: { + variant: "default", + }, + }, ); const Toast = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Root>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & - VariantProps<typeof toastVariants> + React.ElementRef<typeof ToastPrimitives.Root>, + React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & + VariantProps<typeof toastVariants> >(({ className, variant, ...props }, ref) => { - return ( - <ToastPrimitives.Root - ref={ref} - className={cn(toastVariants({ variant }), className)} - {...props} - /> - ); + return ( + <ToastPrimitives.Root + ref={ref} + className={cn(toastVariants({ variant }), className)} + {...props} + /> + ); }); Toast.displayName = ToastPrimitives.Root.displayName; const ToastAction = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Action>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action> + React.ElementRef<typeof ToastPrimitives.Action>, + React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action> >(({ className, ...props }, ref) => ( - <ToastPrimitives.Action - ref={ref} - className={cn( - "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus-visible:outline-hidden focus-visible:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 hover:group-[.destructive]:border-destructive/30 hover:group-[.destructive]:bg-destructive hover:group-[.destructive]:text-destructive-foreground group-[.destructive]:focus-visible::ring-destructive", - className - )} - {...props} - /> + <ToastPrimitives.Action + ref={ref} + className={cn( + "hover:bg-secondary focus:ring-ring group-[.destructive]:border-muted/40 hover:group-[.destructive]:border-destructive/30 hover:group-[.destructive]:bg-destructive hover:group-[.destructive]:text-destructive-foreground group-[.destructive]:focus-visible::ring-destructive inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50", + className, + )} + {...props} + /> )); ToastAction.displayName = ToastPrimitives.Action.displayName; const ToastClose = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Close>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close> + React.ElementRef<typeof ToastPrimitives.Close>, + React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close> >(({ className, ...props }, ref) => ( - <ToastPrimitives.Close - ref={ref} - className={cn( - "absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-hidden focus-visible::ring-1 group-hover:opacity-100 group-[.destructive]:text-muted-foreground hover:group-[.destructive]:text-red-50 focus-visible:group-[.destructive]:ring-red-400 focus-visible:group-[.destructive]:ring-offset-red-600", - className - )} - toast-close="" - {...props} - > - <X className="h-4 w-4" /> - </ToastPrimitives.Close> + <ToastPrimitives.Close + ref={ref} + className={cn( + "text-foreground/50 hover:text-foreground focus-visible::ring-1 group-[.destructive]:text-muted-foreground absolute top-1 right-1 rounded-md p-1 opacity-0 group-hover:opacity-100 hover:group-[.destructive]:text-red-50 focus:opacity-100 focus:outline-hidden focus-visible:group-[.destructive]:ring-red-400 focus-visible:group-[.destructive]:ring-offset-red-600", + className, + )} + toast-close="" + {...props} + > + <X className="size-4" /> + </ToastPrimitives.Close> )); ToastClose.displayName = ToastPrimitives.Close.displayName; const ToastTitle = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Title>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> + React.ElementRef<typeof ToastPrimitives.Title>, + React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> >(({ className, ...props }, ref) => ( - <ToastPrimitives.Title - ref={ref} - className={cn("text-sm font-semibold [&+div]:text-xs", className)} - {...props} - /> + <ToastPrimitives.Title + ref={ref} + className={cn("text-sm font-semibold [&+div]:text-xs", className)} + {...props} + /> )); ToastTitle.displayName = ToastPrimitives.Title.displayName; const ToastDescription = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Description>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description> + React.ElementRef<typeof ToastPrimitives.Description>, + React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description> >(({ className, ...props }, ref) => ( - <ToastPrimitives.Description - ref={ref} - className={cn("text-sm opacity-90", className)} - {...props} - /> + <ToastPrimitives.Description + ref={ref} + className={cn("text-sm opacity-90", className)} + {...props} + /> )); ToastDescription.displayName = ToastPrimitives.Description.displayName; @@ -117,13 +117,13 @@ type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>; type ToastActionElement = React.ReactElement<typeof ToastAction>; export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, }; diff --git a/apps/web/src/components/ui/toaster.tsx b/apps/web/src/components/ui/toaster.tsx deleted file mode 100644 index baf623f0..00000000 --- a/apps/web/src/components/ui/toaster.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import { useToast } from "../../hooks/use-toast"; -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from "./toast"; - -export function Toaster() { - const { toasts } = useToast(); - - return ( - <ToastProvider> - {toasts.map(({ id, title, description, action, ...props }) => ( - <Toast key={id} {...props}> - <div className="grid gap-1"> - {title && <ToastTitle>{title}</ToastTitle>} - {description && <ToastDescription>{description}</ToastDescription>} - </div> - {action} - <ToastClose /> - </Toast> - ))} - <ToastViewport /> - </ToastProvider> - ); -} diff --git a/apps/web/src/components/ui/toggle-group.tsx b/apps/web/src/components/ui/toggle-group.tsx index 5842a07d..66368d50 100644 --- a/apps/web/src/components/ui/toggle-group.tsx +++ b/apps/web/src/components/ui/toggle-group.tsx @@ -2,58 +2,58 @@ import * as React from "react"; import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"; -import { type VariantProps } from "class-variance-authority"; +import type { VariantProps } from "class-variance-authority"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; import { toggleVariants } from "./toggle"; const ToggleGroupContext = React.createContext< - VariantProps<typeof toggleVariants> + VariantProps<typeof toggleVariants> >({ - size: "default", - variant: "default", + size: "default", + variant: "default", }); const ToggleGroup = React.forwardRef< - React.ElementRef<typeof ToggleGroupPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & - VariantProps<typeof toggleVariants> + React.ElementRef<typeof ToggleGroupPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & + VariantProps<typeof toggleVariants> >(({ className, variant, size, children, ...props }, ref) => ( - <ToggleGroupPrimitive.Root - ref={ref} - className={cn("flex items-center justify-center gap-1", className)} - {...props} - > - <ToggleGroupContext.Provider value={{ variant, size }}> - {children} - </ToggleGroupContext.Provider> - </ToggleGroupPrimitive.Root> + <ToggleGroupPrimitive.Root + ref={ref} + className={cn("flex items-center justify-center gap-1", className)} + {...props} + > + <ToggleGroupContext.Provider value={{ variant, size }}> + {children} + </ToggleGroupContext.Provider> + </ToggleGroupPrimitive.Root> )); ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName; const ToggleGroupItem = React.forwardRef< - React.ElementRef<typeof ToggleGroupPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & - VariantProps<typeof toggleVariants> + React.ElementRef<typeof ToggleGroupPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & + VariantProps<typeof toggleVariants> >(({ className, children, variant, size, ...props }, ref) => { - const context = React.useContext(ToggleGroupContext); + const context = React.useContext(ToggleGroupContext); - return ( - <ToggleGroupPrimitive.Item - ref={ref} - className={cn( - toggleVariants({ - variant: context.variant || variant, - size: context.size || size, - }), - className - )} - {...props} - > - {children} - </ToggleGroupPrimitive.Item> - ); + return ( + <ToggleGroupPrimitive.Item + ref={ref} + className={cn( + toggleVariants({ + variant: context.variant || variant, + size: context.size || size, + }), + className, + )} + {...props} + > + {children} + </ToggleGroupPrimitive.Item> + ); }); ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName; diff --git a/apps/web/src/components/ui/toggle.tsx b/apps/web/src/components/ui/toggle.tsx index 460aa9f2..836d81ec 100644 --- a/apps/web/src/components/ui/toggle.tsx +++ b/apps/web/src/components/ui/toggle.tsx @@ -4,40 +4,40 @@ import * as React from "react"; import { Toggle as TogglePrimitive } from "radix-ui"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "../../lib/utils"; +import { cn } from "@/utils/ui"; const toggleVariants = cva( - "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - { - variants: { - variant: { - default: "bg-transparent", - outline: - "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground", - }, - size: { - default: "h-9 px-2 min-w-9", - sm: "h-8 px-1.5 min-w-8", - lg: "h-10 px-2.5 min-w-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } + "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-transparent", + outline: + "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground", + }, + size: { + default: "h-9 px-2 min-w-9", + sm: "h-8 px-1.5 min-w-8", + lg: "h-10 px-2.5 min-w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, ); const Toggle = React.forwardRef< - React.ElementRef<typeof TogglePrimitive.Root>, - React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & - VariantProps<typeof toggleVariants> + React.ElementRef<typeof TogglePrimitive.Root>, + React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & + VariantProps<typeof toggleVariants> >(({ className, variant, size, ...props }, ref) => ( - <TogglePrimitive.Root - ref={ref} - className={cn(toggleVariants({ variant, size, className }))} - {...props} - /> + <TogglePrimitive.Root + ref={ref} + className={cn(toggleVariants({ variant, size, className }))} + {...props} + /> )); Toggle.displayName = TogglePrimitive.Root.displayName; diff --git a/apps/web/src/components/ui/tooltip.tsx b/apps/web/src/components/ui/tooltip.tsx index bfb75e2e..769ac360 100644 --- a/apps/web/src/components/ui/tooltip.tsx +++ b/apps/web/src/components/ui/tooltip.tsx @@ -1,8 +1,8 @@ -import { cva, type VariantProps } from 'class-variance-authority'; -import { Tooltip as TooltipPrimitive } from 'radix-ui'; -import * as React from 'react'; +import { cva, type VariantProps } from "class-variance-authority"; +import { Tooltip as TooltipPrimitive } from "radix-ui"; +import * as React from "react"; -import { cn } from '@/lib/utils'; +import { cn } from "@/utils/ui"; const TooltipProvider = TooltipPrimitive.Provider; @@ -11,62 +11,65 @@ const Tooltip = TooltipPrimitive.Root; const TooltipTrigger = TooltipPrimitive.Trigger; const tooltipVariants = cva( - 'z-50 overflow-visible rounded-sm text-sm shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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', - { - variants: { - variant: { - default: 'bg-popover text-popover-foreground border px-3 py-1.5', - destructive: - 'bg-destructive/10 text-destructive dark:bg-destructive/20 border-destructive [border-width:0.5px]', - outline: 'border-border', - important: - 'bg-amber-100/90 text-amber-900 dark:bg-amber-900/20 dark:text-amber-300 border-amber-900 [border-width:0.5px]', - promotions: - 'bg-red-100/90 text-red-900 dark:bg-red-900/20 dark:text-red-300 border-red-900 [border-width:0.5px]', - personal: - 'bg-green-100/90 text-green-900 dark:bg-green-900/20 dark:text-green-300 border-green-900 [border-width:0.5px]', - updates: - 'bg-purple-100/90 text-purple-900 dark:bg-purple-900/20 dark:text-purple-300 border-purple-900 [border-width:0.5px]', - forums: - 'bg-blue-100/90 text-blue-900 dark:bg-blue-900/20 dark:text-blue-300 border-blue-900 [border-width:0.5px]', - sidebar: 'bg-white dark:bg-[#413F3E] p-2.5 flex flex-col gap-2', - }, - }, - defaultVariants: { - variant: 'default', - }, - }, + "z-50 overflow-visible rounded-sm text-sm shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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", + { + variants: { + variant: { + default: "bg-popover text-popover-foreground border px-3 py-1.5", + destructive: + "bg-destructive/10 text-destructive dark:bg-destructive/20 border-destructive [border-width:0.5px]", + outline: "border-border", + important: + "bg-amber-100/90 text-amber-900 dark:bg-amber-900/20 dark:text-amber-300 border-amber-900 [border-width:0.5px]", + promotions: + "bg-red-100/90 text-red-900 dark:bg-red-900/20 dark:text-red-300 border-red-900 [border-width:0.5px]", + personal: + "bg-green-100/90 text-green-900 dark:bg-green-900/20 dark:text-green-300 border-green-900 [border-width:0.5px]", + updates: + "bg-purple-100/90 text-purple-900 dark:bg-purple-900/20 dark:text-purple-300 border-purple-900 [border-width:0.5px]", + forums: + "bg-blue-100/90 text-blue-900 dark:bg-blue-900/20 dark:text-blue-300 border-blue-900 [border-width:0.5px]", + sidebar: "bg-white dark:bg-[#413F3E] p-2.5 flex flex-col gap-2", + }, + }, + defaultVariants: { + variant: "default", + }, + }, ); interface TooltipContentProps - extends React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>, - VariantProps<typeof tooltipVariants> {} + extends React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>, + VariantProps<typeof tooltipVariants> {} const TooltipContent = React.forwardRef< - React.ElementRef<typeof TooltipPrimitive.Content>, - TooltipContentProps + React.ElementRef<typeof TooltipPrimitive.Content>, + TooltipContentProps >(({ className, sideOffset = 4, variant, ...props }, ref) => ( - <TooltipPrimitive.Content - ref={ref} - sideOffset={sideOffset} - className={cn(tooltipVariants({ variant }), className)} - {...props} - > - {variant === 'sidebar' && ( - <svg - width="6" - height="10" - viewBox="0 0 6 10" - fill="none" - xmlns="http://www.w3.org/2000/svg" - className="absolute left-[-6px] top-1/2 -translate-y-1/2" - > - <path d="M6 0L0 5L6 10V0Z" className="fill-white/80 dark:fill-[#413F3E]" /> - </svg> - )} - {props.children} - </TooltipPrimitive.Content> + <TooltipPrimitive.Content + ref={ref} + sideOffset={sideOffset} + className={cn(tooltipVariants({ variant }), className)} + {...props} + > + {variant === "sidebar" && ( + <svg + width="6" + height="10" + viewBox="0 0 6 10" + fill="none" + xmlns="http://www.w3.org/2000/svg" + className="absolute top-1/2 left-[-6px] -translate-y-1/2" + > + <path + d="M6 0L0 5L6 10V0Z" + className="fill-white/80 dark:fill-[#413F3E]" + /> + </svg> + )} + {props.children} + </TooltipPrimitive.Content> )); TooltipContent.displayName = TooltipPrimitive.Content.displayName; -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
\ No newline at end of file +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/apps/web/src/components/ui/video-player.tsx b/apps/web/src/components/ui/video-player.tsx deleted file mode 100644 index 99e787ef..00000000 --- a/apps/web/src/components/ui/video-player.tsx +++ /dev/null @@ -1,133 +0,0 @@ -"use client"; - -import { useRef, useEffect } from "react"; -import { usePlaybackStore } from "@/stores/playback-store"; - -interface VideoPlayerProps { - src: string; - poster?: string; - className?: string; - clipStartTime: number; - trimStart: number; - trimEnd: number; - clipDuration: number; - trackMuted?: boolean; -} - -export function VideoPlayer({ - src, - poster, - className = "", - clipStartTime, - trimStart, - trimEnd, - clipDuration, - trackMuted = false, -}: VideoPlayerProps) { - const videoRef = useRef<HTMLVideoElement>(null); - const { isPlaying, currentTime, volume, speed, muted } = usePlaybackStore(); - - // Calculate if we're within this clip's timeline range - const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd); - const isInClipRange = - currentTime >= clipStartTime && currentTime < clipEndTime; - - // Sync playback events - useEffect(() => { - const video = videoRef.current; - if (!video || !isInClipRange) return; - - const handleSeekEvent = (e: CustomEvent) => { - // Always update video time, even if outside clip range - const timelineTime = e.detail.time; - const videoTime = Math.max( - trimStart, - Math.min( - clipDuration - trimEnd, - timelineTime - clipStartTime + trimStart - ) - ); - video.currentTime = videoTime; - }; - - const handleUpdateEvent = (e: CustomEvent) => { - // Always update video time, even if outside clip range - const timelineTime = e.detail.time; - const targetTime = Math.max( - trimStart, - Math.min( - clipDuration - trimEnd, - timelineTime - clipStartTime + trimStart - ) - ); - - if (Math.abs(video.currentTime - targetTime) > 0.5) { - video.currentTime = targetTime; - } - }; - - const handleSpeed = (e: CustomEvent) => { - video.playbackRate = e.detail.speed; - }; - - window.addEventListener("playback-seek", handleSeekEvent as EventListener); - window.addEventListener( - "playback-update", - handleUpdateEvent as EventListener - ); - window.addEventListener("playback-speed", handleSpeed as EventListener); - - return () => { - window.removeEventListener( - "playback-seek", - handleSeekEvent as EventListener - ); - window.removeEventListener( - "playback-update", - handleUpdateEvent as EventListener - ); - window.removeEventListener( - "playback-speed", - handleSpeed as EventListener - ); - }; - }, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]); - - // Sync playback state - useEffect(() => { - const video = videoRef.current; - if (!video) return; - - if (isPlaying && isInClipRange) { - video.play().catch(() => {}); - } else { - video.pause(); - } - }, [isPlaying, isInClipRange]); - - // Sync volume and speed - useEffect(() => { - const video = videoRef.current; - if (!video) return; - - video.volume = volume; - video.muted = muted || trackMuted; - video.playbackRate = speed; - }, [volume, speed, muted, trackMuted]); - - return ( - <video - ref={videoRef} - src={src} - poster={poster} - className={`max-w-full max-h-full object-contain ${className}`} - playsInline - preload="auto" - controls={false} - disablePictureInPicture - disableRemotePlayback - style={{ pointerEvents: "none" }} - onContextMenu={(e) => e.preventDefault()} - /> - ); -} |
