diff --git a/frontend/components.json b/frontend/components.json index da828e9e8..e6938bbd2 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -4,7 +4,7 @@ "rsc": false, "tsx": true, "tailwind": { - "config": "tailwind.config.js", + "config": "", "css": "src/index.css", "baseColor": "zinc", "cssVariables": true, diff --git a/frontend/package.json b/frontend/package.json index f292e4c57..e4def652b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,25 +20,10 @@ "prepare": "cd .. && husky frontend/.husky" }, "dependencies": { + "@fontsource-variable/figtree": "^5.2.10", + "@fontsource-variable/inter": "^5.2.8", "@getalby/lightning-tools": "^6.0.0", "@getalby/sdk": "^7.0.0", - "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-alert-dialog": "^1.1.14", - "@radix-ui/react-avatar": "^1.1.11", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-navigation-menu": "^1.2.13", - "@radix-ui/react-popover": "^1.1.14", - "@radix-ui/react-progress": "^1.1.7", - "@radix-ui/react-radio-group": "^1.3.8", - "@radix-ui/react-select": "^2.2.5", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.12", - "@radix-ui/react-tooltip": "^1.2.7", "@scure/bip39": "^2.0.1", "@stepperize/react": "^5.1.9", "argon2-wasm-esm": "^1.0.3", @@ -47,11 +32,13 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "compare-versions": "^6.1.1", + "date-fns": "^4.1.0", "dayjs": "^1.11.10", "embla-carousel-react": "^8.6.0", - "lucide-react": "^0.544.0", + "lucide-react": "^0.577.0", + "radix-ui": "^1.4.3", "react": "18.3.1", - "react-day-picker": "^9.11.0", + "react-day-picker": "^9.14.0", "react-dom": "18.3.1", "react-lottie": "^1.2.4", "react-qr-code": "^2.0.12", @@ -92,5 +79,6 @@ "vite": "^5.4.0", "vite-plugin-pwa": "^1.2.0", "vite-tsconfig-paths": "^6.1.1" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/frontend/public/fonts/Inter-italic.var.woff2 b/frontend/public/fonts/Inter-italic.var.woff2 deleted file mode 100644 index 03875311a..000000000 Binary files a/frontend/public/fonts/Inter-italic.var.woff2 and /dev/null differ diff --git a/frontend/public/fonts/Inter-roman.var.woff2 b/frontend/public/fonts/Inter-roman.var.woff2 deleted file mode 100644 index a6efdc486..000000000 Binary files a/frontend/public/fonts/Inter-roman.var.woff2 and /dev/null differ diff --git a/frontend/src/components/AppSidebar.tsx b/frontend/src/components/AppSidebar.tsx index cd8bb2332..16e328253 100644 --- a/frontend/src/components/AppSidebar.tsx +++ b/frontend/src/components/AppSidebar.tsx @@ -127,7 +127,7 @@ export function AppSidebar() {
setOpenMobile(false)}> - +
diff --git a/frontend/src/components/icons/AlbyHubLogo.tsx b/frontend/src/components/icons/AlbyHubLogo.tsx index 15a9a8e5c..777df7b89 100644 --- a/frontend/src/components/icons/AlbyHubLogo.tsx +++ b/frontend/src/components/icons/AlbyHubLogo.tsx @@ -1,24 +1,51 @@ import { SVGAttributes } from "react"; +import { cn } from "src/lib/utils"; interface AlbyHubLogoProps extends SVGAttributes { - invert?: boolean; + monochrome?: boolean; } -export function AlbyHubLogo({ invert = false, ...props }: AlbyHubLogoProps) { +export function AlbyHubLogo({ + monochrome = false, + className, + style, + ...props +}: AlbyHubLogoProps) { return ( - - + {/* Ink paths */} + + + + + + + + {/* Accent paths */} + + + + + + ); } diff --git a/frontend/src/components/layouts/TwoColumnFullScreenLayout.tsx b/frontend/src/components/layouts/TwoColumnFullScreenLayout.tsx index b655a9cc2..11eb246fd 100644 --- a/frontend/src/components/layouts/TwoColumnFullScreenLayout.tsx +++ b/frontend/src/components/layouts/TwoColumnFullScreenLayout.tsx @@ -85,7 +85,7 @@ export default function TwoColumnFullScreenLayout() { />
- + {info?.version && (

{info.version}

)} diff --git a/frontend/src/components/stepper.tsx b/frontend/src/components/stepper.tsx index e5923e7da..b888b0558 100644 --- a/frontend/src/components/stepper.tsx +++ b/frontend/src/components/stepper.tsx @@ -1,4 +1,4 @@ -import { Slot } from "@radix-ui/react-slot"; +import { Slot } from "radix-ui"; import * as Stepperize from "@stepperize/react"; import { type VariantProps, cva } from "class-variance-authority"; import * as React from "react"; @@ -226,13 +226,15 @@ const defineStepper = ( Title, Description, Panel: ({ children, asChild, ...props }) => { - const Comp = asChild ? Slot : "div"; + const Comp = asChild ? Slot.Root : "div"; const { tracking } = useStepperProvider(); return ( scrollIntoStepperPanel(node, tracking)} + ref={(node: HTMLDivElement | null) => + scrollIntoStepperPanel(node, tracking) + } {...props} > {children} @@ -240,7 +242,7 @@ const defineStepper = ( ); }, Controls: ({ children, className, asChild, ...props }) => { - const Comp = asChild ? Slot : "div"; + const Comp = asChild ? Slot.Root : "div"; return ( & { asChild?: boolean }) => { - const Comp = asChild ? Slot : "h4"; + const Comp = asChild ? Slot.Root : "h4"; return ( & { asChild?: boolean }) => { - const Comp = asChild ? Slot : "p"; + const Comp = asChild ? Slot.Root : "p"; return ( svg]:rotate-180", + "flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180", className )} {...props} > {children} - + ); @@ -53,7 +53,7 @@ function AccordionContent({ return (
{children}
diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx index 119fbb494..98d1ef408 100644 --- a/frontend/src/components/ui/alert-dialog.tsx +++ b/frontend/src/components/ui/alert-dialog.tsx @@ -1,10 +1,10 @@ "use client"; -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; import * as React from "react"; +import { AlertDialog as AlertDialogPrimitive } from "radix-ui"; import { cn } from "src/lib/utils"; -import { buttonVariants } from "./buttonVariants"; +import { Button } from "src/components/ui/button"; function AlertDialog({ ...props @@ -36,7 +36,7 @@ function AlertDialogOverlay({ ) { +}: React.ComponentProps & { + size?: "default" | "sm"; +}) { return ( ); @@ -84,7 +91,7 @@ function AlertDialogFooter({
); @@ -112,33 +122,61 @@ function AlertDialogDescription({ return ( ); } -function AlertDialogAction({ +function AlertDialogMedia({ className, ...props -}: React.ComponentProps) { +}: React.ComponentProps<"div">) { return ( - ); } +function AlertDialogAction({ + className, + variant = "default", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ); +} + function AlertDialogCancel({ className, + variant = "outline", + size = "default", ...props -}: React.ComponentProps) { +}: React.ComponentProps & + Pick, "variant" | "size">) { return ( - + ); } @@ -150,6 +188,7 @@ export { AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, + AlertDialogMedia, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, diff --git a/frontend/src/components/ui/alert.tsx b/frontend/src/components/ui/alert.tsx index 608321fed..0913fe5ec 100644 --- a/frontend/src/components/ui/alert.tsx +++ b/frontend/src/components/ui/alert.tsx @@ -1,20 +1,18 @@ -import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "src/lib/utils"; const alertVariants = cva( - "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", { variants: { variant: { default: "bg-card text-card-foreground", destructive: - "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", - - /* Custom variants */ + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current", warning: - "border-warning-foreground border text-warning-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-warning-foreground/90", + "border-warning-foreground border text-warning-foreground *:data-[slot=alert-description]:text-warning-foreground/90 [&>svg]:text-current", }, }, defaultVariants: { @@ -59,7 +57,7 @@ function AlertDescription({
) { +}: React.ComponentProps & { + size?: "default" | "sm" | "lg"; +}) { return ( ) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ); +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className + )} + {...props} + /> + ); +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarBadge, + AvatarGroup, + AvatarGroupCount, +}; diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx index 7c16bd9bd..4f949f77e 100644 --- a/frontend/src/components/ui/badge.tsx +++ b/frontend/src/components/ui/badge.tsx @@ -1,48 +1,23 @@ -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; +import { type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; import { cn } from "src/lib/utils"; - -const badgeVariants = cva( - "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none 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 transition-[color,box-shadow] overflow-hidden", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - - /* Custom variants */ - positive: - "border-positive-foreground bg-positive text-positive-foreground [a&]:hover:bg-positive/90", - warning: - "border-warning-foreground bg-warning text-warning-foreground [a&]:hover:bg-warning/90", - }, - }, - defaultVariants: { - variant: "default", - }, - } -); +import { badgeVariants } from "src/components/ui/badgeVariants"; function Badge({ className, - variant, + variant = "default", asChild = false, ...props }: React.ComponentProps<"span"> & VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span"; + const Comp = asChild ? Slot.Root : "span"; return ( diff --git a/frontend/src/components/ui/badgeVariants.tsx b/frontend/src/components/ui/badgeVariants.tsx new file mode 100644 index 000000000..a9054c7b3 --- /dev/null +++ b/frontend/src/components/ui/badgeVariants.tsx @@ -0,0 +1,27 @@ +import { cva } from "class-variance-authority"; + +export const badgeVariants = cva( + "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", + outline: + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + link: "text-primary underline-offset-4 [a&]:hover:underline", + positive: + "border-positive-foreground bg-positive text-positive-foreground [a&]:hover:bg-positive/90", + warning: + "border-warning-foreground bg-warning text-warning-foreground [a&]:hover:bg-warning/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); diff --git a/frontend/src/components/ui/breadcrumb.tsx b/frontend/src/components/ui/breadcrumb.tsx index 77d5bad10..9eed05074 100644 --- a/frontend/src/components/ui/breadcrumb.tsx +++ b/frontend/src/components/ui/breadcrumb.tsx @@ -1,6 +1,7 @@ -import { Slot } from "@radix-ui/react-slot"; -import { ChevronRightIcon, EllipsisVerticalIcon } from "lucide-react"; import * as React from "react"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; +import { Slot } from "radix-ui"; + import { cn } from "src/lib/utils"; function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { @@ -12,7 +13,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
    & { asChild?: boolean; }) { - const Comp = asChild ? Slot : "a"; + const Comp = asChild ? Slot.Root : "a"; return ( ); @@ -55,7 +56,7 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) { role="link" aria-disabled="true" aria-current="page" - className={cn("text-foreground font-normal", className)} + className={cn("font-normal text-foreground", className)} {...props} /> ); @@ -74,7 +75,7 @@ function BreadcrumbSeparator({ className={cn("[&>svg]:size-3.5", className)} {...props} > - {children ?? } + {children ?? } ); } @@ -91,7 +92,7 @@ function BreadcrumbEllipsis({ className={cn("flex size-9 items-center justify-center", className)} {...props} > - + More ); @@ -99,10 +100,10 @@ function BreadcrumbEllipsis({ export { Breadcrumb, - BreadcrumbEllipsis, + BreadcrumbList, BreadcrumbItem, BreadcrumbLink, - BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, + BreadcrumbEllipsis, }; diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index a43e9b950..34fc3006b 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -1,25 +1,27 @@ -import { Slot } from "@radix-ui/react-slot"; -import { type VariantProps } from "class-variance-authority"; import * as React from "react"; -import { buttonVariants } from "src/components/ui/buttonVariants"; +import { type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; import { cn } from "src/lib/utils"; +import { buttonVariants } from "src/components/ui/buttonVariants"; function Button({ className, - variant, - size, + variant = "default", + size = "default", asChild = false, ...props }: React.ComponentProps<"button"> & VariantProps & { asChild?: boolean; }) { - const Comp = asChild ? Slot : "button"; + const Comp = asChild ? Slot.Root : "button"; return ( diff --git a/frontend/src/components/ui/buttonVariants.tsx b/frontend/src/components/ui/buttonVariants.tsx index 92ee26404..07727e6b4 100644 --- a/frontend/src/components/ui/buttonVariants.tsx +++ b/frontend/src/components/ui/buttonVariants.tsx @@ -1,16 +1,17 @@ import { cva } from "class-variance-authority"; +/** Shared with `Button`, `LinkButton`, and responsive button wrappers. */ export const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none 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", + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: @@ -22,9 +23,13 @@ export const buttonVariants = cva( }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", }, }, defaultVariants: { diff --git a/frontend/src/components/ui/calendar.tsx b/frontend/src/components/ui/calendar.tsx index 3dfb4785c..d6d03cb6f 100644 --- a/frontend/src/components/ui/calendar.tsx +++ b/frontend/src/components/ui/calendar.tsx @@ -1,14 +1,18 @@ +import * as React from "react"; import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, } from "lucide-react"; -import * as React from "react"; -import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"; +import { + DayPicker, + getDefaultClassNames, + type DayButton, +} from "react-day-picker"; -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 Calendar({ className, @@ -28,7 +32,7 @@ function Calendar({ svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, className @@ -42,64 +46,67 @@ function Calendar({ classNames={{ root: cn("w-fit", defaultClassNames.root), months: cn( - "flex gap-4 flex-col md:flex-row relative", + "relative flex flex-col gap-4 md:flex-row", defaultClassNames.months ), - month: cn("flex flex-col w-full gap-4", defaultClassNames.month), + month: cn("flex w-full flex-col gap-4", defaultClassNames.month), nav: cn( - "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", + "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", defaultClassNames.nav ), button_previous: cn( buttonVariants({ variant: buttonVariant }), - "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", defaultClassNames.button_previous ), button_next: cn( buttonVariants({ variant: buttonVariant }), - "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", defaultClassNames.button_next ), month_caption: cn( - "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", + "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)", defaultClassNames.month_caption ), dropdowns: cn( - "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", + "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium", defaultClassNames.dropdowns ), dropdown_root: cn( - "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", + "relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50", defaultClassNames.dropdown_root ), dropdown: cn( - "absolute bg-popover inset-0 opacity-0", + "absolute inset-0 bg-popover opacity-0", defaultClassNames.dropdown ), caption_label: cn( - "select-none font-medium", + "font-medium select-none", captionLayout === "label" ? "text-sm" - : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", + : "flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground", defaultClassNames.caption_label ), table: "w-full border-collapse", weekdays: cn("flex", defaultClassNames.weekdays), weekday: cn( - "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", + "flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none", defaultClassNames.weekday ), - week: cn("flex w-full mt-2", defaultClassNames.week), + week: cn("mt-2 flex w-full", defaultClassNames.week), week_number_header: cn( - "select-none w-(--cell-size)", + "w-(--cell-size) select-none", defaultClassNames.week_number_header ), week_number: cn( - "text-[0.8rem] select-none text-muted-foreground", + "text-[0.8rem] text-muted-foreground select-none", defaultClassNames.week_number ), day: cn( - "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", + "group/day relative aspect-square h-full w-full p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-md", + props.showWeekNumber + ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md" + : "[&:first-child[data-selected=true]_button]:rounded-l-md", defaultClassNames.day ), range_start: cn( @@ -109,7 +116,7 @@ function Calendar({ range_middle: cn("rounded-none", defaultClassNames.range_middle), range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), today: cn( - "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", + "rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none", defaultClassNames.today ), outside: cn( @@ -187,8 +194,10 @@ function CalendarDayButton({ }, [modifiers.focused]); return ( - + + )} +
); } @@ -121,7 +138,7 @@ function DialogDescription({ return ( ); diff --git a/frontend/src/components/ui/dropdown-menu.tsx b/frontend/src/components/ui/dropdown-menu.tsx index 5a8c52467..62bce114e 100644 --- a/frontend/src/components/ui/dropdown-menu.tsx +++ b/frontend/src/components/ui/dropdown-menu.tsx @@ -1,8 +1,6 @@ -"use client"; - import * as React from "react"; -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; +import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; import { cn } from "src/lib/utils"; @@ -42,7 +40,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" 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 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", + "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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} @@ -74,7 +72,7 @@ function DropdownMenuItem({ data-inset={inset} data-variant={variant} className={cn( - "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", className )} {...props} @@ -92,7 +90,7 @@ function DropdownMenuCheckboxItem({ ); @@ -184,7 +182,7 @@ function DropdownMenuShortcut({ ) { } const fieldVariants = cva( - "group/field data-[invalid=true]:text-destructive flex w-full gap-3", + "group/field flex w-full gap-3 data-[invalid=true]:text-destructive", { variants: { orientation: { @@ -61,10 +63,10 @@ const fieldVariants = cva( horizontal: [ "flex-row items-center", "[&>[data-slot=field-label]]:flex-auto", - "has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start", + "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", ], responsive: [ - "@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto", + "flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto", "@md/field-group:[&>[data-slot=field-label]]:flex-auto", "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", ], @@ -114,8 +116,8 @@ function FieldLabel({ data-slot="field-label" className={cn( "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50", - "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>[data-slot=field]]:p-4", - "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10", + "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4", + "has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10", className )} {...props} @@ -128,7 +130,7 @@ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
) {

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..8a0296a37 100644 --- a/frontend/src/components/ui/sonner.tsx +++ b/frontend/src/components/ui/sonner.tsx @@ -1,24 +1,34 @@ -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 { Toaster as Sonner, type ToasterProps } from "sonner"; +import { useTheme } from "./theme-provider"; const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme(); + const { isDarkMode } = useTheme(); + const theme: ToasterProps["theme"] = isDarkMode ? "dark" : "light"; 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">) {