) {
a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
+ "text-sm leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
+ "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
+ "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
)}
{...props}
@@ -171,7 +173,7 @@ function FieldSeparator({
{children && (
{children}
@@ -194,17 +196,21 @@ function FieldError({
return children;
}
- if (!errors) {
+ if (!errors?.length) {
return null;
}
- if (errors?.length === 1 && errors[0]?.message) {
- return errors[0].message;
+ const uniqueErrors = [
+ ...new Map(errors.map((error) => [error?.message, error])).values(),
+ ];
+
+ if (uniqueErrors?.length == 1) {
+ return uniqueErrors[0]?.message;
}
return (
- {errors.map(
+ {uniqueErrors.map(
(error, index) =>
error?.message && - {error.message}
)}
@@ -220,7 +226,7 @@ function FieldError({
{content}
diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx
index 20ad0f607..fc4bd988e 100644
--- a/frontend/src/components/ui/input.tsx
+++ b/frontend/src/components/ui/input.tsx
@@ -8,9 +8,9 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
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-transparent 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-[3px]",
- "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx
index 12d1472a1..355e26336 100644
--- a/frontend/src/components/ui/label.tsx
+++ b/frontend/src/components/ui/label.tsx
@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
-import * as LabelPrimitive from "@radix-ui/react-label";
+import { Label as LabelPrimitive } from "radix-ui";
import { cn } from "src/lib/utils";
diff --git a/frontend/src/components/ui/navigation-menu.tsx b/frontend/src/components/ui/navigation-menu.tsx
index d9c30e56e..2c037f99a 100644
--- a/frontend/src/components/ui/navigation-menu.tsx
+++ b/frontend/src/components/ui/navigation-menu.tsx
@@ -1,7 +1,7 @@
import * as React from "react";
-import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDownIcon } from "lucide-react";
+import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui";
import { cn } from "src/lib/utils";
@@ -59,7 +59,7 @@ function NavigationMenuItem({
}
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 hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
+ "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-[color,box-shadow] outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent/50 data-[state=open]:text-accent-foreground data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
);
function NavigationMenuTrigger({
@@ -90,8 +90,8 @@ function NavigationMenuContent({
-
+
);
}
diff --git a/frontend/src/components/ui/pagination.tsx b/frontend/src/components/ui/pagination.tsx
index 6beaa58b2..6bd7b7776 100644
--- a/frontend/src/components/ui/pagination.tsx
+++ b/frontend/src/components/ui/pagination.tsx
@@ -1,13 +1,13 @@
+import * as React from "react";
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react";
-import * as React from "react";
-import { Button } from "src/components/ui/button";
import { cn } from "src/lib/utils";
-import { buttonVariants } from "./buttonVariants";
+import { Button } from "src/components/ui/button";
+import { buttonVariants } from "src/components/ui/buttonVariants";
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
@@ -120,9 +120,9 @@ function PaginationEllipsis({
export {
Pagination,
PaginationContent,
- PaginationEllipsis,
- PaginationItem,
PaginationLink,
- PaginationNext,
+ PaginationItem,
PaginationPrevious,
+ PaginationNext,
+ PaginationEllipsis,
};
diff --git a/frontend/src/components/ui/popover.tsx b/frontend/src/components/ui/popover.tsx
index 76ba637a1..bfb0f1e67 100644
--- a/frontend/src/components/ui/popover.tsx
+++ b/frontend/src/components/ui/popover.tsx
@@ -1,7 +1,5 @@
-"use client";
-
import * as React from "react";
-import * as PopoverPrimitive from "@radix-ui/react-popover";
+import { Popover as PopoverPrimitive } from "radix-ui";
import { cn } from "src/lib/utils";
@@ -30,7 +28,7 @@ function PopoverContent({
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 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
+ "z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden 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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
className
)}
{...props}
@@ -45,4 +43,45 @@ function PopoverAnchor({
return ;
}
-export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
+function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
+ return (
+
+ );
+}
+
+function PopoverDescription({
+ className,
+ ...props
+}: React.ComponentProps<"p">) {
+ return (
+
+ );
+}
+
+export {
+ Popover,
+ PopoverTrigger,
+ PopoverContent,
+ PopoverAnchor,
+ PopoverHeader,
+ PopoverTitle,
+ PopoverDescription,
+};
diff --git a/frontend/src/components/ui/progress.tsx b/frontend/src/components/ui/progress.tsx
index 50d0083fc..66d09d74f 100644
--- a/frontend/src/components/ui/progress.tsx
+++ b/frontend/src/components/ui/progress.tsx
@@ -1,5 +1,7 @@
-import * as ProgressPrimitive from "@radix-ui/react-progress";
+"use client";
+
import * as React from "react";
+import { Progress as ProgressPrimitive } from "radix-ui";
import { cn } from "src/lib/utils";
@@ -12,14 +14,14 @@ function Progress({
diff --git a/frontend/src/components/ui/radio-group.tsx b/frontend/src/components/ui/radio-group.tsx
index 91625f45a..654e713f8 100644
--- a/frontend/src/components/ui/radio-group.tsx
+++ b/frontend/src/components/ui/radio-group.tsx
@@ -1,8 +1,6 @@
-"use client";
-
import * as React from "react";
-import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { CircleIcon } from "lucide-react";
+import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
import { cn } from "src/lib/utils";
@@ -27,7 +25,7 @@ function RadioGroupItem({
-
+
);
diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx
index 5c0ceb1bd..c784a22bd 100644
--- a/frontend/src/components/ui/select.tsx
+++ b/frontend/src/components/ui/select.tsx
@@ -1,6 +1,8 @@
+"use client";
+
import * as React from "react";
-import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
+import { Select as SelectPrimitive } from "radix-ui";
import { cn } from "src/lib/utils";
@@ -35,7 +37,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
- "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
className
)}
{...props}
@@ -51,7 +53,8 @@ function SelectTrigger({
function SelectContent({
className,
children,
- position = "popper",
+ position = "item-aligned",
+ align = "center",
...props
}: React.ComponentProps) {
return (
@@ -59,12 +62,13 @@ function SelectContent({
@@ -90,7 +94,7 @@ function SelectLabel({
return (
);
@@ -105,12 +109,15 @@ function SelectItem({
-
+
@@ -127,7 +134,7 @@ function SelectSeparator({
return (
);
diff --git a/frontend/src/components/ui/separator.tsx b/frontend/src/components/ui/separator.tsx
index 93acafd46..64528b49e 100644
--- a/frontend/src/components/ui/separator.tsx
+++ b/frontend/src/components/ui/separator.tsx
@@ -1,7 +1,5 @@
-"use client";
-
import * as React from "react";
-import * as SeparatorPrimitive from "@radix-ui/react-separator";
+import { Separator as SeparatorPrimitive } from "radix-ui";
import { cn } from "src/lib/utils";
@@ -17,7 +15,7 @@ function Separator({
decorative={decorative}
orientation={orientation}
className={cn(
- "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
+ "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
diff --git a/frontend/src/components/ui/sheet.tsx b/frontend/src/components/ui/sheet.tsx
index cc3862730..f4e153bc9 100644
--- a/frontend/src/components/ui/sheet.tsx
+++ b/frontend/src/components/ui/sheet.tsx
@@ -1,6 +1,8 @@
-import * as SheetPrimitive from "@radix-ui/react-dialog";
-import { XIcon } from "lucide-react";
+"use client";
+
import * as React from "react";
+import { XIcon } from "lucide-react";
+import { Dialog as SheetPrimitive } from "radix-ui";
import { cn } from "src/lib/utils";
@@ -34,7 +36,7 @@ function SheetOverlay({
& {
side?: "top" | "right" | "bottom" | "left";
+ showCloseButton?: boolean;
}) {
return (
@@ -56,24 +60,26 @@ function SheetContent({
{children}
-
-
- Close
-
+ {showCloseButton && (
+
+
+ Close
+
+ )}
);
@@ -106,7 +112,7 @@ function SheetTitle({
return (
);
@@ -119,7 +125,7 @@ function SheetDescription({
return (
);
@@ -127,11 +133,11 @@ function SheetDescription({
export {
Sheet,
+ SheetTrigger,
SheetClose,
SheetContent,
- SheetDescription,
- SheetFooter,
SheetHeader,
+ SheetFooter,
SheetTitle,
- SheetTrigger,
+ SheetDescription,
};
diff --git a/frontend/src/components/ui/sidebar.tsx b/frontend/src/components/ui/sidebar.tsx
index a10e9d3e0..00c62c739 100644
--- a/frontend/src/components/ui/sidebar.tsx
+++ b/frontend/src/components/ui/sidebar.tsx
@@ -1,10 +1,10 @@
-"use client";
-
-import { Slot } from "@radix-ui/react-slot";
-import { cva, VariantProps } from "class-variance-authority";
-import { PanelLeftIcon } from "lucide-react";
import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+import { PanelLeftIcon } from "lucide-react";
+import { Slot } from "radix-ui";
+import { useIsMobile } from "src/hooks/use-mobile";
+import { cn } from "src/lib/utils";
import { Button } from "src/components/ui/button";
import { Input } from "src/components/ui/input";
import { Separator } from "src/components/ui/separator";
@@ -22,8 +22,6 @@ import {
TooltipProvider,
TooltipTrigger,
} from "src/components/ui/tooltip";
-import { useIsMobile } from "src/hooks/use-mobile";
-import { cn } from "src/lib/utils";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
@@ -139,7 +137,7 @@ function SidebarProvider({
} as React.CSSProperties
}
className={cn(
- "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
+ "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
className
)}
{...props}
@@ -170,7 +168,7 @@ function Sidebar({
{children}
@@ -291,10 +289,10 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
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",
+ "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] hover:after:bg-sidebar-border 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",
+ "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
@@ -309,7 +307,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
);
@@ -362,7 +360,7 @@ function SidebarSeparator({
);
@@ -398,14 +396,14 @@ function SidebarGroupLabel({
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
- const Comp = asChild ? Slot : "div";
+ const Comp = asChild ? Slot.Root : "div";
return (
svg]:size-4 [&>svg]:shrink-0",
+ "flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 ring-sidebar-ring 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
)}
@@ -419,14 +417,14 @@ function SidebarGroupAction({
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
- const Comp = asChild ? Slot : "button";
+ const Comp = asChild ? Slot.Root : "button";
return (
svg]:size-4 [&>svg]:shrink-0",
+ "absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden 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",
@@ -474,7 +472,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
}
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",
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! 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 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 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
@@ -508,7 +506,7 @@ function SidebarMenuButton({
isActive?: boolean;
tooltip?: string | React.ComponentProps;
} & VariantProps) {
- const Comp = asChild ? Slot : "button";
+ const Comp = asChild ? Slot.Root : "button";
const { isMobile, state } = useSidebar();
const button = (
@@ -554,14 +552,14 @@ function SidebarMenuAction({
asChild?: boolean;
showOnHover?: boolean;
}) {
- const Comp = asChild ? Slot : "button";
+ const Comp = asChild ? Slot.Root : "button";
return (
svg]:size-4 [&>svg]:shrink-0",
+ "absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform peer-hover/menu-button:text-sidebar-accent-foreground 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",
"peer-data-[size=sm]/menu-button:top-1",
@@ -569,7 +567,7 @@ function SidebarMenuAction({
"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",
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground data-[state=open]:opacity-100 md:opacity-0",
className
)}
{...props}
@@ -586,7 +584,7 @@ function SidebarMenuBadge({
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",
+ "pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium text-sidebar-foreground 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",
@@ -606,11 +604,8 @@ function SidebarMenuSkeleton({
}: React.ComponentProps<"div"> & {
showIcon?: boolean;
}) {
- // Random width between 50 to 90%.
- const width = React.useMemo(() => {
- // eslint-disable-next-line react-hooks/purity
- return `${Math.floor(Math.random() * 40) + 50}%`;
- }, []);
+ /* Stable width for skeleton placeholder (avoids impure Math.random in render). */
+ const width = "70%";
return (
) {
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",
+ "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
)}
@@ -678,7 +673,7 @@ function SidebarMenuSubButton({
size?: "sm" | "md";
isActive?: boolean;
}) {
- const Comp = asChild ? Slot : "a";
+ const Comp = asChild ? Slot.Root : "a";
return (
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",
+ "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden 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",
@@ -723,6 +718,6 @@ export {
SidebarRail,
SidebarSeparator,
SidebarTrigger,
- // eslint-disable-next-line react-refresh/only-export-components
+ // eslint-disable-next-line react-refresh/only-export-components -- hook colocated with sidebar UI
useSidebar,
};
diff --git a/frontend/src/components/ui/skeleton.tsx b/frontend/src/components/ui/skeleton.tsx
index 3b999bc8c..f951bc00b 100644
--- a/frontend/src/components/ui/skeleton.tsx
+++ b/frontend/src/components/ui/skeleton.tsx
@@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
);
diff --git a/frontend/src/components/ui/sonner.tsx b/frontend/src/components/ui/sonner.tsx
index 19e6821ad..95cc9421b 100644
--- a/frontend/src/components/ui/sonner.tsx
+++ b/frontend/src/components/ui/sonner.tsx
@@ -1,24 +1,51 @@
-import { Toaster as Sonner, ToasterProps } from "sonner";
-import { useTheme } from "src/components/ui/theme-provider";
+import {
+ CircleCheckIcon,
+ InfoIcon,
+ Loader2Icon,
+ OctagonXIcon,
+ TriangleAlertIcon,
+} from "lucide-react";
+import * as React from "react";
+import { Toaster as Sonner, type ToasterProps } from "sonner";
+
+/** Sync with Hub `ThemeProvider` (`.dark` on `document.documentElement`), not next-themes. */
+function useDocumentDarkTheme(): ToasterProps["theme"] {
+ const [theme, setTheme] = React.useState("light");
+
+ React.useEffect(() => {
+ const el = document.documentElement;
+ const sync = () => {
+ setTheme(el.classList.contains("dark") ? "dark" : "light");
+ };
+ sync();
+ const observer = new MutationObserver(sync);
+ observer.observe(el, { attributes: true, attributeFilter: ["class"] });
+ return () => observer.disconnect();
+ }, []);
+
+ return theme;
+}
const Toaster = ({ ...props }: ToasterProps) => {
- const { theme = "system" } = useTheme();
+ const theme = useDocumentDarkTheme();
return (
,
+ info: ,
+ warning: ,
+ error: ,
+ loading: ,
+ }}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
- "--error-bg": "var(--destructive)",
- "--error-text": "var(--destructive-foreground)",
- "--error-border": "var(--destructive)",
- "--success-bg": "var(--popover)",
- "--success-text": "var(--popover-foreground)",
- "--success-border": "var(--border)",
+ "--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
diff --git a/frontend/src/components/ui/switch.tsx b/frontend/src/components/ui/switch.tsx
index cb0b7a242..a5d577643 100644
--- a/frontend/src/components/ui/switch.tsx
+++ b/frontend/src/components/ui/switch.tsx
@@ -1,19 +1,23 @@
"use client";
import * as React from "react";
-import * as SwitchPrimitive from "@radix-ui/react-switch";
+import { Switch as SwitchPrimitive } from "radix-ui";
import { cn } from "src/lib/utils";
function Switch({
className,
+ size = "default",
...props
-}: React.ComponentProps) {
+}: React.ComponentProps & {
+ size?: "sm" | "default";
+}) {
return (
diff --git a/frontend/src/components/ui/table.tsx b/frontend/src/components/ui/table.tsx
index 27b3078c1..7800ae2c7 100644
--- a/frontend/src/components/ui/table.tsx
+++ b/frontend/src/components/ui/table.tsx
@@ -42,7 +42,7 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
tr]:last:border-b-0",
+ "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
@@ -55,7 +55,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
) {
| [role=checkbox]]:translate-y-[2px]",
+ "h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
@@ -96,7 +96,7 @@ function TableCaption({
return (
);
diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx
index 820e25e68..16a434e6d 100644
--- a/frontend/src/components/ui/tabs.tsx
+++ b/frontend/src/components/ui/tabs.tsx
@@ -1,53 +1,91 @@
+"use client";
+
import * as React from "react";
-import * as TabsPrimitive from "@radix-ui/react-tabs";
+import { cva, type VariantProps } from "class-variance-authority";
+import { Tabs as TabsPrimitive } from "radix-ui";
import { cn } from "src/lib/utils";
-const Tabs = TabsPrimitive.Root;
-
-const TabsList = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-TabsList.displayName = TabsPrimitive.List.displayName;
-
-const TabsTrigger = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
-
-const TabsContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-TabsContent.displayName = TabsPrimitive.Content.displayName;
+function Tabs({
+ className,
+ orientation = "horizontal",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+const tabsListVariants = cva(
+ "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
+ {
+ variants: {
+ variant: {
+ default: "bg-muted",
+ line: "gap-1 bg-transparent",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+);
+
+function TabsList({
+ className,
+ variant = "default",
+ ...props
+}: React.ComponentProps &
+ VariantProps) {
+ return (
+
+ );
+}
+
+function TabsTrigger({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function TabsContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/frontend/src/components/ui/textarea.tsx b/frontend/src/components/ui/textarea.tsx
index 402d3b634..f639f1103 100644
--- a/frontend/src/components/ui/textarea.tsx
+++ b/frontend/src/components/ui/textarea.tsx
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|