Spaces:
Sleeping
Sleeping
Commit ·
eaf815d
1
Parent(s): 56d7f80
Improve sidebar
Browse files- ui/app/api/discovery/route.ts +1 -1
- ui/app/api/explorer/route.ts +1 -1
- ui/app/layout.tsx +16 -8
- ui/app/not-found.tsx +48 -0
- ui/components/animate-ui/components/animate/tooltip.tsx +74 -0
- ui/components/animate-ui/components/radix/dropdown-menu.tsx +200 -0
- ui/components/animate-ui/components/radix/sheet.tsx +155 -0
- ui/components/animate-ui/components/radix/sidebar.tsx +810 -0
- ui/components/animate-ui/primitives/animate/slot.tsx +96 -0
- ui/components/animate-ui/primitives/animate/tooltip.tsx +572 -0
- ui/components/animate-ui/primitives/effects/highlight.tsx +640 -0
- ui/components/animate-ui/primitives/radix/checkbox.tsx +128 -0
- ui/components/animate-ui/primitives/radix/collapsible.tsx +11 -0
- ui/components/animate-ui/primitives/radix/sheet.tsx +191 -0
- ui/components/page-header.tsx +60 -8
- ui/components/sidebar.tsx +283 -63
- ui/components/ui/avatar.tsx +50 -0
- ui/components/ui/breadcrumb.tsx +115 -0
- ui/components/ui/button.tsx +36 -30
- ui/components/ui/empty.tsx +104 -0
- ui/eslint.config.mjs +63 -5
- ui/package.json +9 -0
- ui/pnpm-lock.yaml +123 -0
ui/app/api/discovery/route.ts
CHANGED
|
@@ -4,7 +4,7 @@ export async function POST(request: Request) {
|
|
| 4 |
const body = await request.json();
|
| 5 |
const { query } = body;
|
| 6 |
|
| 7 |
-
console.
|
| 8 |
|
| 9 |
// Here you would typically call your Python backend
|
| 10 |
// e.g., await fetch('http://localhost:8000/api/discovery', { ... })
|
|
|
|
| 4 |
const body = await request.json();
|
| 5 |
const { query } = body;
|
| 6 |
|
| 7 |
+
console.info("Starting discovery for:", query);
|
| 8 |
|
| 9 |
// Here you would typically call your Python backend
|
| 10 |
// e.g., await fetch('http://localhost:8000/api/discovery', { ... })
|
ui/app/api/explorer/route.ts
CHANGED
|
@@ -11,7 +11,7 @@ export async function GET(request: Request) {
|
|
| 11 |
searchParams.get("colorBy") || undefined
|
| 12 |
);
|
| 13 |
return NextResponse.json(data);
|
| 14 |
-
} catch
|
| 15 |
return NextResponse.json({ error: "Invalid parameters" }, { status: 400 });
|
| 16 |
}
|
| 17 |
}
|
|
|
|
| 11 |
searchParams.get("colorBy") || undefined
|
| 12 |
);
|
| 13 |
return NextResponse.json(data);
|
| 14 |
+
} catch {
|
| 15 |
return NextResponse.json({ error: "Invalid parameters" }, { status: 400 });
|
| 16 |
}
|
| 17 |
}
|
ui/app/layout.tsx
CHANGED
|
@@ -1,7 +1,11 @@
|
|
| 1 |
import type { Metadata, Viewport } from "next";
|
| 2 |
import { Geist, Geist_Mono } from "next/font/google";
|
| 3 |
import "./globals.css";
|
| 4 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
const geistSans = Geist({
|
| 7 |
variable: "--font-geist-sans",
|
|
@@ -41,14 +45,18 @@ export default function RootLayout({
|
|
| 41 |
return (
|
| 42 |
<html lang="en">
|
| 43 |
<body
|
| 44 |
-
className={`${geistSans.variable} ${geistMono.variable} antialiased
|
| 45 |
>
|
| 46 |
-
<
|
| 47 |
-
|
| 48 |
-
<
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
</body>
|
| 53 |
</html>
|
| 54 |
);
|
|
|
|
| 1 |
import type { Metadata, Viewport } from "next";
|
| 2 |
import { Geist, Geist_Mono } from "next/font/google";
|
| 3 |
import "./globals.css";
|
| 4 |
+
import { AppSidebar } from "@/components/sidebar";
|
| 5 |
+
import {
|
| 6 |
+
SidebarProvider,
|
| 7 |
+
SidebarInset,
|
| 8 |
+
} from "@/components/animate-ui/components/radix/sidebar";
|
| 9 |
|
| 10 |
const geistSans = Geist({
|
| 11 |
variable: "--font-geist-sans",
|
|
|
|
| 45 |
return (
|
| 46 |
<html lang="en">
|
| 47 |
<body
|
| 48 |
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
| 49 |
>
|
| 50 |
+
<SidebarProvider>
|
| 51 |
+
<AppSidebar />
|
| 52 |
+
<SidebarInset>
|
| 53 |
+
<main className="flex-1 overflow-y-auto bg-background p-8">
|
| 54 |
+
<div className="mx-auto max-w-7xl">
|
| 55 |
+
{children}
|
| 56 |
+
</div>
|
| 57 |
+
</main>
|
| 58 |
+
</SidebarInset>
|
| 59 |
+
</SidebarProvider>
|
| 60 |
</body>
|
| 61 |
</html>
|
| 62 |
);
|
ui/app/not-found.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Button } from "@/components/ui/button";
|
| 2 |
+
import {
|
| 3 |
+
Empty,
|
| 4 |
+
EmptyContent,
|
| 5 |
+
EmptyDescription,
|
| 6 |
+
EmptyHeader,
|
| 7 |
+
EmptyTitle,
|
| 8 |
+
} from "@/components/ui/empty";
|
| 9 |
+
import { Compass, Home } from "lucide-react";
|
| 10 |
+
|
| 11 |
+
export function NotFoundPage() {
|
| 12 |
+
return (
|
| 13 |
+
<div className="flex w-full items-center justify-center">
|
| 14 |
+
<div className="flex h-screen items-center border-x">
|
| 15 |
+
<div>
|
| 16 |
+
<div className="absolute inset-x-0 h-px bg-border" />
|
| 17 |
+
<Empty>
|
| 18 |
+
<EmptyHeader>
|
| 19 |
+
<EmptyTitle className="font-black font-mono text-8xl">
|
| 20 |
+
404
|
| 21 |
+
</EmptyTitle>
|
| 22 |
+
<EmptyDescription className="text-nowrap">
|
| 23 |
+
The page you're looking for might have been <br />
|
| 24 |
+
moved or doesn't exist.
|
| 25 |
+
</EmptyDescription>
|
| 26 |
+
</EmptyHeader>
|
| 27 |
+
<EmptyContent>
|
| 28 |
+
<div className="flex gap-2">
|
| 29 |
+
<Button asChild>
|
| 30 |
+
<a href="#">
|
| 31 |
+
<Home /> Go Home
|
| 32 |
+
</a>
|
| 33 |
+
</Button>
|
| 34 |
+
|
| 35 |
+
<Button asChild variant="outline">
|
| 36 |
+
<a href="#">
|
| 37 |
+
<Compass /> Explore
|
| 38 |
+
</a>
|
| 39 |
+
</Button>
|
| 40 |
+
</div>
|
| 41 |
+
</EmptyContent>
|
| 42 |
+
</Empty>
|
| 43 |
+
<div className="absolute inset-x-0 h-px bg-border" />
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
);
|
| 48 |
+
}
|
ui/components/animate-ui/components/animate/tooltip.tsx
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from 'react';
|
| 2 |
+
import * as motion from 'motion/react-client';
|
| 3 |
+
|
| 4 |
+
import {
|
| 5 |
+
TooltipProvider as TooltipProviderPrimitive,
|
| 6 |
+
Tooltip as TooltipPrimitive,
|
| 7 |
+
TooltipTrigger as TooltipTriggerPrimitive,
|
| 8 |
+
TooltipContent as TooltipContentPrimitive,
|
| 9 |
+
TooltipArrow as TooltipArrowPrimitive,
|
| 10 |
+
type TooltipProviderProps as TooltipProviderPrimitiveProps,
|
| 11 |
+
type TooltipProps as TooltipPrimitiveProps,
|
| 12 |
+
type TooltipTriggerProps as TooltipTriggerPrimitiveProps,
|
| 13 |
+
type TooltipContentProps as TooltipContentPrimitiveProps,
|
| 14 |
+
} from '@/components/animate-ui/primitives/animate/tooltip';
|
| 15 |
+
import { cn } from '@/lib/utils';
|
| 16 |
+
|
| 17 |
+
type TooltipProviderProps = TooltipProviderPrimitiveProps;
|
| 18 |
+
|
| 19 |
+
function TooltipProvider({ openDelay = 0, ...props }: TooltipProviderProps) {
|
| 20 |
+
return <TooltipProviderPrimitive openDelay={openDelay} {...props} />;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
type TooltipProps = TooltipPrimitiveProps;
|
| 24 |
+
|
| 25 |
+
function Tooltip({ sideOffset = 10, ...props }: TooltipProps) {
|
| 26 |
+
return <TooltipPrimitive sideOffset={sideOffset} {...props} />;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
type TooltipTriggerProps = TooltipTriggerPrimitiveProps;
|
| 30 |
+
|
| 31 |
+
function TooltipTrigger({ ...props }: TooltipTriggerProps) {
|
| 32 |
+
return <TooltipTriggerPrimitive {...props} />;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
type TooltipContentProps = Omit<TooltipContentPrimitiveProps, 'asChild'> & {
|
| 36 |
+
children: React.ReactNode;
|
| 37 |
+
layout?: boolean | 'position' | 'size' | 'preserve-aspect';
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
function TooltipContent({
|
| 41 |
+
className,
|
| 42 |
+
children,
|
| 43 |
+
layout = 'preserve-aspect',
|
| 44 |
+
...props
|
| 45 |
+
}: TooltipContentProps) {
|
| 46 |
+
return (
|
| 47 |
+
<TooltipContentPrimitive
|
| 48 |
+
className={cn(
|
| 49 |
+
'z-50 w-fit bg-primary text-primary-foreground rounded-md',
|
| 50 |
+
className,
|
| 51 |
+
)}
|
| 52 |
+
{...props}
|
| 53 |
+
>
|
| 54 |
+
<motion.div className="overflow-hidden px-3 py-1.5 text-xs text-balance">
|
| 55 |
+
<motion.div layout={layout}>{children}</motion.div>
|
| 56 |
+
</motion.div>
|
| 57 |
+
<TooltipArrowPrimitive
|
| 58 |
+
className="fill-primary size-3 data-[side='bottom']:translate-y-[1px] data-[side='right']:translate-x-[1px] data-[side='left']:translate-x-[-1px] data-[side='top']:translate-y-[-1px]"
|
| 59 |
+
tipRadius={2}
|
| 60 |
+
/>
|
| 61 |
+
</TooltipContentPrimitive>
|
| 62 |
+
);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
export {
|
| 66 |
+
TooltipProvider,
|
| 67 |
+
Tooltip,
|
| 68 |
+
TooltipTrigger,
|
| 69 |
+
TooltipContent,
|
| 70 |
+
type TooltipProviderProps,
|
| 71 |
+
type TooltipProps,
|
| 72 |
+
type TooltipTriggerProps,
|
| 73 |
+
type TooltipContentProps,
|
| 74 |
+
};
|
ui/components/animate-ui/components/radix/dropdown-menu.tsx
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
| 5 |
+
import { Check, ChevronRight, Circle } from "lucide-react"
|
| 6 |
+
|
| 7 |
+
import { cn } from "@/lib/utils"
|
| 8 |
+
|
| 9 |
+
const DropdownMenu = DropdownMenuPrimitive.Root
|
| 10 |
+
|
| 11 |
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
| 12 |
+
|
| 13 |
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
| 14 |
+
|
| 15 |
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
| 16 |
+
|
| 17 |
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
| 18 |
+
|
| 19 |
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
| 20 |
+
|
| 21 |
+
const DropdownMenuSubTrigger = React.forwardRef<
|
| 22 |
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
| 23 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
| 24 |
+
inset?: boolean
|
| 25 |
+
}
|
| 26 |
+
>(({ className, inset, children, ...props }, ref) => (
|
| 27 |
+
<DropdownMenuPrimitive.SubTrigger
|
| 28 |
+
ref={ref}
|
| 29 |
+
className={cn(
|
| 30 |
+
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
| 31 |
+
inset && "pl-8",
|
| 32 |
+
className
|
| 33 |
+
)}
|
| 34 |
+
{...props}
|
| 35 |
+
>
|
| 36 |
+
{children}
|
| 37 |
+
<ChevronRight className="ml-auto" />
|
| 38 |
+
</DropdownMenuPrimitive.SubTrigger>
|
| 39 |
+
))
|
| 40 |
+
DropdownMenuSubTrigger.displayName =
|
| 41 |
+
DropdownMenuPrimitive.SubTrigger.displayName
|
| 42 |
+
|
| 43 |
+
const DropdownMenuSubContent = React.forwardRef<
|
| 44 |
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
| 45 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
| 46 |
+
>(({ className, ...props }, ref) => (
|
| 47 |
+
<DropdownMenuPrimitive.SubContent
|
| 48 |
+
ref={ref}
|
| 49 |
+
className={cn(
|
| 50 |
+
"z-50 min-w-[8rem] 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",
|
| 51 |
+
className
|
| 52 |
+
)}
|
| 53 |
+
{...props}
|
| 54 |
+
/>
|
| 55 |
+
))
|
| 56 |
+
DropdownMenuSubContent.displayName =
|
| 57 |
+
DropdownMenuPrimitive.SubContent.displayName
|
| 58 |
+
|
| 59 |
+
const DropdownMenuContent = React.forwardRef<
|
| 60 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
| 61 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
| 62 |
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
| 63 |
+
<DropdownMenuPrimitive.Portal>
|
| 64 |
+
<DropdownMenuPrimitive.Content
|
| 65 |
+
ref={ref}
|
| 66 |
+
sideOffset={sideOffset}
|
| 67 |
+
className={cn(
|
| 68 |
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 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",
|
| 69 |
+
className
|
| 70 |
+
)}
|
| 71 |
+
{...props}
|
| 72 |
+
/>
|
| 73 |
+
</DropdownMenuPrimitive.Portal>
|
| 74 |
+
))
|
| 75 |
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
| 76 |
+
|
| 77 |
+
const DropdownMenuItem = React.forwardRef<
|
| 78 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
| 79 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
| 80 |
+
inset?: boolean
|
| 81 |
+
}
|
| 82 |
+
>(({ className, inset, ...props }, ref) => (
|
| 83 |
+
<DropdownMenuPrimitive.Item
|
| 84 |
+
ref={ref}
|
| 85 |
+
className={cn(
|
| 86 |
+
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
| 87 |
+
inset && "pl-8",
|
| 88 |
+
className
|
| 89 |
+
)}
|
| 90 |
+
{...props}
|
| 91 |
+
/>
|
| 92 |
+
))
|
| 93 |
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
| 94 |
+
|
| 95 |
+
const DropdownMenuCheckboxItem = React.forwardRef<
|
| 96 |
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
| 97 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
| 98 |
+
>(({ className, children, checked, ...props }, ref) => (
|
| 99 |
+
<DropdownMenuPrimitive.CheckboxItem
|
| 100 |
+
ref={ref}
|
| 101 |
+
className={cn(
|
| 102 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
| 103 |
+
className
|
| 104 |
+
)}
|
| 105 |
+
checked={checked}
|
| 106 |
+
{...props}
|
| 107 |
+
>
|
| 108 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 109 |
+
<DropdownMenuPrimitive.ItemIndicator>
|
| 110 |
+
<Check className="h-4 w-4" />
|
| 111 |
+
</DropdownMenuPrimitive.ItemIndicator>
|
| 112 |
+
</span>
|
| 113 |
+
{children}
|
| 114 |
+
</DropdownMenuPrimitive.CheckboxItem>
|
| 115 |
+
))
|
| 116 |
+
DropdownMenuCheckboxItem.displayName =
|
| 117 |
+
DropdownMenuPrimitive.CheckboxItem.displayName
|
| 118 |
+
|
| 119 |
+
const DropdownMenuRadioItem = React.forwardRef<
|
| 120 |
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
| 121 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
| 122 |
+
>(({ className, children, ...props }, ref) => (
|
| 123 |
+
<DropdownMenuPrimitive.RadioItem
|
| 124 |
+
ref={ref}
|
| 125 |
+
className={cn(
|
| 126 |
+
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
| 127 |
+
className
|
| 128 |
+
)}
|
| 129 |
+
{...props}
|
| 130 |
+
>
|
| 131 |
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
| 132 |
+
<DropdownMenuPrimitive.ItemIndicator>
|
| 133 |
+
<Circle className="h-2 w-2 fill-current" />
|
| 134 |
+
</DropdownMenuPrimitive.ItemIndicator>
|
| 135 |
+
</span>
|
| 136 |
+
{children}
|
| 137 |
+
</DropdownMenuPrimitive.RadioItem>
|
| 138 |
+
))
|
| 139 |
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
| 140 |
+
|
| 141 |
+
const DropdownMenuLabel = React.forwardRef<
|
| 142 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
| 143 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
| 144 |
+
inset?: boolean
|
| 145 |
+
}
|
| 146 |
+
>(({ className, inset, ...props }, ref) => (
|
| 147 |
+
<DropdownMenuPrimitive.Label
|
| 148 |
+
ref={ref}
|
| 149 |
+
className={cn(
|
| 150 |
+
"px-2 py-1.5 text-sm font-semibold",
|
| 151 |
+
inset && "pl-8",
|
| 152 |
+
className
|
| 153 |
+
)}
|
| 154 |
+
{...props}
|
| 155 |
+
/>
|
| 156 |
+
))
|
| 157 |
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
| 158 |
+
|
| 159 |
+
const DropdownMenuSeparator = React.forwardRef<
|
| 160 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
| 161 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
| 162 |
+
>(({ className, ...props }, ref) => (
|
| 163 |
+
<DropdownMenuPrimitive.Separator
|
| 164 |
+
ref={ref}
|
| 165 |
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
| 166 |
+
{...props}
|
| 167 |
+
/>
|
| 168 |
+
))
|
| 169 |
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
| 170 |
+
|
| 171 |
+
const DropdownMenuShortcut = ({
|
| 172 |
+
className,
|
| 173 |
+
...props
|
| 174 |
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
| 175 |
+
return (
|
| 176 |
+
<span
|
| 177 |
+
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
| 178 |
+
{...props}
|
| 179 |
+
/>
|
| 180 |
+
)
|
| 181 |
+
}
|
| 182 |
+
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
| 183 |
+
|
| 184 |
+
export {
|
| 185 |
+
DropdownMenu,
|
| 186 |
+
DropdownMenuTrigger,
|
| 187 |
+
DropdownMenuContent,
|
| 188 |
+
DropdownMenuItem,
|
| 189 |
+
DropdownMenuCheckboxItem,
|
| 190 |
+
DropdownMenuRadioItem,
|
| 191 |
+
DropdownMenuLabel,
|
| 192 |
+
DropdownMenuSeparator,
|
| 193 |
+
DropdownMenuShortcut,
|
| 194 |
+
DropdownMenuGroup,
|
| 195 |
+
DropdownMenuPortal,
|
| 196 |
+
DropdownMenuSub,
|
| 197 |
+
DropdownMenuSubContent,
|
| 198 |
+
DropdownMenuSubTrigger,
|
| 199 |
+
DropdownMenuRadioGroup,
|
| 200 |
+
}
|
ui/components/animate-ui/components/radix/sheet.tsx
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from 'react';
|
| 2 |
+
|
| 3 |
+
import {
|
| 4 |
+
Sheet as SheetPrimitive,
|
| 5 |
+
SheetTrigger as SheetTriggerPrimitive,
|
| 6 |
+
SheetOverlay as SheetOverlayPrimitive,
|
| 7 |
+
SheetClose as SheetClosePrimitive,
|
| 8 |
+
SheetPortal as SheetPortalPrimitive,
|
| 9 |
+
SheetContent as SheetContentPrimitive,
|
| 10 |
+
SheetHeader as SheetHeaderPrimitive,
|
| 11 |
+
SheetFooter as SheetFooterPrimitive,
|
| 12 |
+
SheetTitle as SheetTitlePrimitive,
|
| 13 |
+
SheetDescription as SheetDescriptionPrimitive,
|
| 14 |
+
type SheetProps as SheetPrimitiveProps,
|
| 15 |
+
type SheetTriggerProps as SheetTriggerPrimitiveProps,
|
| 16 |
+
type SheetOverlayProps as SheetOverlayPrimitiveProps,
|
| 17 |
+
type SheetCloseProps as SheetClosePrimitiveProps,
|
| 18 |
+
type SheetContentProps as SheetContentPrimitiveProps,
|
| 19 |
+
type SheetHeaderProps as SheetHeaderPrimitiveProps,
|
| 20 |
+
type SheetFooterProps as SheetFooterPrimitiveProps,
|
| 21 |
+
type SheetTitleProps as SheetTitlePrimitiveProps,
|
| 22 |
+
type SheetDescriptionProps as SheetDescriptionPrimitiveProps,
|
| 23 |
+
} from '@/components/animate-ui/primitives/radix/sheet';
|
| 24 |
+
import { cn } from '@/lib/utils';
|
| 25 |
+
import { XIcon } from 'lucide-react';
|
| 26 |
+
|
| 27 |
+
type SheetProps = SheetPrimitiveProps;
|
| 28 |
+
|
| 29 |
+
function Sheet(props: SheetProps) {
|
| 30 |
+
return <SheetPrimitive {...props} />;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
type SheetTriggerProps = SheetTriggerPrimitiveProps;
|
| 34 |
+
|
| 35 |
+
function SheetTrigger(props: SheetTriggerProps) {
|
| 36 |
+
return <SheetTriggerPrimitive {...props} />;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
type SheetOverlayProps = SheetOverlayPrimitiveProps;
|
| 40 |
+
|
| 41 |
+
function SheetOverlay({ className, ...props }: SheetOverlayProps) {
|
| 42 |
+
return (
|
| 43 |
+
<SheetOverlayPrimitive
|
| 44 |
+
className={cn('fixed inset-0 z-50 bg-black/50', className)}
|
| 45 |
+
{...props}
|
| 46 |
+
/>
|
| 47 |
+
);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
type SheetCloseProps = SheetClosePrimitiveProps;
|
| 51 |
+
|
| 52 |
+
function SheetClose(props: SheetCloseProps) {
|
| 53 |
+
return <SheetClosePrimitive {...props} />;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
type SheetContentProps = SheetContentPrimitiveProps & {
|
| 57 |
+
showCloseButton?: boolean;
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
function SheetContent({
|
| 61 |
+
className,
|
| 62 |
+
children,
|
| 63 |
+
side = 'right',
|
| 64 |
+
showCloseButton = true,
|
| 65 |
+
...props
|
| 66 |
+
}: SheetContentProps) {
|
| 67 |
+
return (
|
| 68 |
+
<SheetPortalPrimitive>
|
| 69 |
+
<SheetOverlay />
|
| 70 |
+
<SheetContentPrimitive
|
| 71 |
+
className={cn(
|
| 72 |
+
'bg-background fixed z-50 flex flex-col gap-4 shadow-lg',
|
| 73 |
+
side === 'right' && 'h-full w-[350px] border-l',
|
| 74 |
+
side === 'left' && 'h-full w-[350px] border-r',
|
| 75 |
+
side === 'top' && 'w-full h-[350px] border-b',
|
| 76 |
+
side === 'bottom' && 'w-full h-[350px] border-t',
|
| 77 |
+
className,
|
| 78 |
+
)}
|
| 79 |
+
side={side}
|
| 80 |
+
{...props}
|
| 81 |
+
>
|
| 82 |
+
{children}
|
| 83 |
+
{showCloseButton && (
|
| 84 |
+
<SheetClose className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
| 85 |
+
<XIcon className="size-4" />
|
| 86 |
+
<span className="sr-only">Close</span>
|
| 87 |
+
</SheetClose>
|
| 88 |
+
)}
|
| 89 |
+
</SheetContentPrimitive>
|
| 90 |
+
</SheetPortalPrimitive>
|
| 91 |
+
);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
type SheetHeaderProps = SheetHeaderPrimitiveProps;
|
| 95 |
+
|
| 96 |
+
function SheetHeader({ className, ...props }: SheetHeaderProps) {
|
| 97 |
+
return (
|
| 98 |
+
<SheetHeaderPrimitive
|
| 99 |
+
className={cn('flex flex-col gap-1.5 p-4', className)}
|
| 100 |
+
{...props}
|
| 101 |
+
/>
|
| 102 |
+
);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
type SheetFooterProps = SheetFooterPrimitiveProps;
|
| 106 |
+
|
| 107 |
+
function SheetFooter({ className, ...props }: SheetFooterProps) {
|
| 108 |
+
return (
|
| 109 |
+
<SheetFooterPrimitive
|
| 110 |
+
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
| 111 |
+
{...props}
|
| 112 |
+
/>
|
| 113 |
+
);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
type SheetTitleProps = SheetTitlePrimitiveProps;
|
| 117 |
+
|
| 118 |
+
function SheetTitle({ className, ...props }: SheetTitleProps) {
|
| 119 |
+
return (
|
| 120 |
+
<SheetTitlePrimitive
|
| 121 |
+
className={cn('text-foreground font-semibold', className)}
|
| 122 |
+
{...props}
|
| 123 |
+
/>
|
| 124 |
+
);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
type SheetDescriptionProps = SheetDescriptionPrimitiveProps;
|
| 128 |
+
|
| 129 |
+
function SheetDescription({ className, ...props }: SheetDescriptionProps) {
|
| 130 |
+
return (
|
| 131 |
+
<SheetDescriptionPrimitive
|
| 132 |
+
className={cn('text-muted-foreground text-sm', className)}
|
| 133 |
+
{...props}
|
| 134 |
+
/>
|
| 135 |
+
);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
export {
|
| 139 |
+
Sheet,
|
| 140 |
+
SheetTrigger,
|
| 141 |
+
SheetClose,
|
| 142 |
+
SheetContent,
|
| 143 |
+
SheetHeader,
|
| 144 |
+
SheetFooter,
|
| 145 |
+
SheetTitle,
|
| 146 |
+
SheetDescription,
|
| 147 |
+
type SheetProps,
|
| 148 |
+
type SheetTriggerProps,
|
| 149 |
+
type SheetCloseProps,
|
| 150 |
+
type SheetContentProps,
|
| 151 |
+
type SheetHeaderProps,
|
| 152 |
+
type SheetFooterProps,
|
| 153 |
+
type SheetTitleProps,
|
| 154 |
+
type SheetDescriptionProps,
|
| 155 |
+
};
|
ui/components/animate-ui/components/radix/sidebar.tsx
ADDED
|
@@ -0,0 +1,810 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import * as React from 'react';
|
| 4 |
+
import { Slot } from 'radix-ui';
|
| 5 |
+
import { cva, VariantProps } from 'class-variance-authority';
|
| 6 |
+
import { PanelLeftIcon } from 'lucide-react';
|
| 7 |
+
import { type Transition } from 'motion/react';
|
| 8 |
+
|
| 9 |
+
import { useIsMobile } from '@/hooks/use-mobile';
|
| 10 |
+
import { cn } from '@/lib/utils';
|
| 11 |
+
import { Button } from '@/components/ui/button';
|
| 12 |
+
import { Input } from '@/components/ui/input';
|
| 13 |
+
import { Separator } from '@/components/ui/separator';
|
| 14 |
+
import { Skeleton } from '@/components/ui/skeleton';
|
| 15 |
+
import {
|
| 16 |
+
Sheet,
|
| 17 |
+
SheetContent,
|
| 18 |
+
SheetDescription,
|
| 19 |
+
SheetHeader,
|
| 20 |
+
SheetTitle,
|
| 21 |
+
} from '@/components/animate-ui/components/radix/sheet';
|
| 22 |
+
import {
|
| 23 |
+
TooltipProvider,
|
| 24 |
+
Tooltip,
|
| 25 |
+
TooltipContent,
|
| 26 |
+
TooltipTrigger,
|
| 27 |
+
} from '@/components/animate-ui/components/animate/tooltip';
|
| 28 |
+
import {
|
| 29 |
+
Highlight,
|
| 30 |
+
HighlightItem,
|
| 31 |
+
} from '@/components/animate-ui/primitives/effects/highlight';
|
| 32 |
+
import { getStrictContext } from '@/lib/get-strict-context';
|
| 33 |
+
|
| 34 |
+
const SIDEBAR_COOKIE_NAME = 'sidebar_state';
|
| 35 |
+
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
| 36 |
+
const SIDEBAR_WIDTH = '16rem';
|
| 37 |
+
const SIDEBAR_WIDTH_MOBILE = '18rem';
|
| 38 |
+
const SIDEBAR_WIDTH_ICON = '3rem';
|
| 39 |
+
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
| 40 |
+
|
| 41 |
+
type SidebarContextProps = {
|
| 42 |
+
state: 'expanded' | 'collapsed';
|
| 43 |
+
open: boolean;
|
| 44 |
+
setOpen: (open: boolean) => void;
|
| 45 |
+
openMobile: boolean;
|
| 46 |
+
setOpenMobile: (open: boolean) => void;
|
| 47 |
+
isMobile: boolean;
|
| 48 |
+
toggleSidebar: () => void;
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
const [LocalSidebarProvider, useSidebar] =
|
| 52 |
+
getStrictContext<SidebarContextProps>('SidebarContext');
|
| 53 |
+
|
| 54 |
+
type SidebarProviderProps = React.ComponentProps<'div'> & {
|
| 55 |
+
defaultOpen?: boolean;
|
| 56 |
+
open?: boolean;
|
| 57 |
+
onOpenChange?: (open: boolean) => void;
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
function SidebarProvider({
|
| 61 |
+
defaultOpen = true,
|
| 62 |
+
open: openProp,
|
| 63 |
+
onOpenChange: setOpenProp,
|
| 64 |
+
className,
|
| 65 |
+
style,
|
| 66 |
+
children,
|
| 67 |
+
...props
|
| 68 |
+
}: SidebarProviderProps) {
|
| 69 |
+
const isMobile = useIsMobile();
|
| 70 |
+
const [openMobile, setOpenMobile] = React.useState(false);
|
| 71 |
+
|
| 72 |
+
// This is the internal state of the sidebar.
|
| 73 |
+
// We use openProp and setOpenProp for control from outside the component.
|
| 74 |
+
const [_open, _setOpen] = React.useState(defaultOpen);
|
| 75 |
+
const open = openProp ?? _open;
|
| 76 |
+
const setOpen = React.useCallback(
|
| 77 |
+
(value: boolean | ((value: boolean) => boolean)) => {
|
| 78 |
+
const openState = typeof value === 'function' ? value(open) : value;
|
| 79 |
+
if (setOpenProp) {
|
| 80 |
+
setOpenProp(openState);
|
| 81 |
+
} else {
|
| 82 |
+
_setOpen(openState);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// This sets the cookie to keep the sidebar state.
|
| 86 |
+
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
| 87 |
+
},
|
| 88 |
+
[setOpenProp, open],
|
| 89 |
+
);
|
| 90 |
+
|
| 91 |
+
// Helper to toggle the sidebar.
|
| 92 |
+
const toggleSidebar = React.useCallback(() => {
|
| 93 |
+
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
|
| 94 |
+
}, [isMobile, setOpen, setOpenMobile]);
|
| 95 |
+
|
| 96 |
+
// Adds a keyboard shortcut to toggle the sidebar.
|
| 97 |
+
React.useEffect(() => {
|
| 98 |
+
const handleKeyDown = (event: KeyboardEvent) => {
|
| 99 |
+
if (
|
| 100 |
+
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
| 101 |
+
(event.metaKey || event.ctrlKey)
|
| 102 |
+
) {
|
| 103 |
+
event.preventDefault();
|
| 104 |
+
toggleSidebar();
|
| 105 |
+
}
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
window.addEventListener('keydown', handleKeyDown);
|
| 109 |
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
| 110 |
+
}, [toggleSidebar]);
|
| 111 |
+
|
| 112 |
+
// We add a state so that we can do data-state="expanded" or "collapsed".
|
| 113 |
+
// This makes it easier to style the sidebar with Tailwind classes.
|
| 114 |
+
const state = open ? 'expanded' : 'collapsed';
|
| 115 |
+
|
| 116 |
+
const contextValue = React.useMemo<SidebarContextProps>(
|
| 117 |
+
() => ({
|
| 118 |
+
state,
|
| 119 |
+
open,
|
| 120 |
+
setOpen,
|
| 121 |
+
isMobile,
|
| 122 |
+
openMobile,
|
| 123 |
+
setOpenMobile,
|
| 124 |
+
toggleSidebar,
|
| 125 |
+
}),
|
| 126 |
+
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
|
| 127 |
+
);
|
| 128 |
+
|
| 129 |
+
return (
|
| 130 |
+
<LocalSidebarProvider value={contextValue}>
|
| 131 |
+
<TooltipProvider openDelay={0}>
|
| 132 |
+
<div
|
| 133 |
+
data-slot="sidebar-wrapper"
|
| 134 |
+
style={
|
| 135 |
+
{
|
| 136 |
+
'--sidebar-width': SIDEBAR_WIDTH,
|
| 137 |
+
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
| 138 |
+
...style,
|
| 139 |
+
} as React.CSSProperties
|
| 140 |
+
}
|
| 141 |
+
className={cn(
|
| 142 |
+
'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
|
| 143 |
+
className,
|
| 144 |
+
)}
|
| 145 |
+
{...props}
|
| 146 |
+
>
|
| 147 |
+
{children}
|
| 148 |
+
</div>
|
| 149 |
+
</TooltipProvider>
|
| 150 |
+
</LocalSidebarProvider>
|
| 151 |
+
);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
type SidebarProps = React.ComponentProps<'div'> & {
|
| 155 |
+
side?: 'left' | 'right';
|
| 156 |
+
variant?: 'sidebar' | 'floating' | 'inset';
|
| 157 |
+
collapsible?: 'offcanvas' | 'icon' | 'none';
|
| 158 |
+
containerClassName?: string;
|
| 159 |
+
animateOnHover?: boolean;
|
| 160 |
+
transition?: Transition;
|
| 161 |
+
};
|
| 162 |
+
|
| 163 |
+
function Sidebar({
|
| 164 |
+
side = 'left',
|
| 165 |
+
variant = 'sidebar',
|
| 166 |
+
collapsible = 'offcanvas',
|
| 167 |
+
className,
|
| 168 |
+
children,
|
| 169 |
+
animateOnHover = true,
|
| 170 |
+
containerClassName,
|
| 171 |
+
transition = { type: 'spring', stiffness: 350, damping: 35 },
|
| 172 |
+
...props
|
| 173 |
+
}: SidebarProps) {
|
| 174 |
+
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
| 175 |
+
|
| 176 |
+
if (collapsible === 'none') {
|
| 177 |
+
return (
|
| 178 |
+
<Highlight
|
| 179 |
+
enabled={animateOnHover}
|
| 180 |
+
hover
|
| 181 |
+
controlledItems
|
| 182 |
+
mode="parent"
|
| 183 |
+
containerClassName={containerClassName}
|
| 184 |
+
transition={transition}
|
| 185 |
+
>
|
| 186 |
+
<div
|
| 187 |
+
data-slot="sidebar"
|
| 188 |
+
className={cn(
|
| 189 |
+
'bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col',
|
| 190 |
+
className,
|
| 191 |
+
)}
|
| 192 |
+
{...props}
|
| 193 |
+
>
|
| 194 |
+
{children}
|
| 195 |
+
</div>
|
| 196 |
+
</Highlight>
|
| 197 |
+
);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
if (isMobile) {
|
| 201 |
+
return (
|
| 202 |
+
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
| 203 |
+
<SheetContent
|
| 204 |
+
data-sidebar="sidebar"
|
| 205 |
+
data-slot="sidebar"
|
| 206 |
+
data-mobile="true"
|
| 207 |
+
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
| 208 |
+
style={
|
| 209 |
+
{
|
| 210 |
+
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
| 211 |
+
} as React.CSSProperties
|
| 212 |
+
}
|
| 213 |
+
side={side}
|
| 214 |
+
>
|
| 215 |
+
<SheetHeader className="sr-only">
|
| 216 |
+
<SheetTitle>Sidebar</SheetTitle>
|
| 217 |
+
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
| 218 |
+
</SheetHeader>
|
| 219 |
+
<Highlight
|
| 220 |
+
enabled={animateOnHover}
|
| 221 |
+
hover
|
| 222 |
+
controlledItems
|
| 223 |
+
mode="parent"
|
| 224 |
+
containerClassName={cn('h-full', containerClassName)}
|
| 225 |
+
transition={transition}
|
| 226 |
+
>
|
| 227 |
+
<div className="flex h-full w-full flex-col">{children}</div>
|
| 228 |
+
</Highlight>
|
| 229 |
+
</SheetContent>
|
| 230 |
+
</Sheet>
|
| 231 |
+
);
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
return (
|
| 235 |
+
<div
|
| 236 |
+
className="group peer text-sidebar-foreground hidden md:block"
|
| 237 |
+
data-state={state}
|
| 238 |
+
data-collapsible={state === 'collapsed' ? collapsible : ''}
|
| 239 |
+
data-variant={variant}
|
| 240 |
+
data-side={side}
|
| 241 |
+
data-slot="sidebar"
|
| 242 |
+
>
|
| 243 |
+
{/* This is what handles the sidebar gap on desktop */}
|
| 244 |
+
<div
|
| 245 |
+
data-slot="sidebar-gap"
|
| 246 |
+
className={cn(
|
| 247 |
+
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-400 ease-[cubic-bezier(0.7,-0.15,0.25,1.15)]',
|
| 248 |
+
'group-data-[collapsible=offcanvas]:w-0',
|
| 249 |
+
'group-data-[side=right]:rotate-180',
|
| 250 |
+
variant === 'floating' || variant === 'inset'
|
| 251 |
+
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
|
| 252 |
+
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
|
| 253 |
+
)}
|
| 254 |
+
/>
|
| 255 |
+
<div
|
| 256 |
+
data-slot="sidebar-container"
|
| 257 |
+
className={cn(
|
| 258 |
+
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-400 ease-[cubic-bezier(0.75,0,0.25,1)] md:flex',
|
| 259 |
+
side === 'left'
|
| 260 |
+
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
| 261 |
+
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
| 262 |
+
// Adjust the padding for floating and inset variants.
|
| 263 |
+
variant === 'floating' || variant === 'inset'
|
| 264 |
+
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
|
| 265 |
+
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
| 266 |
+
className,
|
| 267 |
+
)}
|
| 268 |
+
{...props}
|
| 269 |
+
>
|
| 270 |
+
<Highlight
|
| 271 |
+
containerClassName={cn('size-full', containerClassName)}
|
| 272 |
+
enabled={animateOnHover}
|
| 273 |
+
hover
|
| 274 |
+
controlledItems
|
| 275 |
+
mode="parent"
|
| 276 |
+
forceUpdateBounds
|
| 277 |
+
transition={transition}
|
| 278 |
+
>
|
| 279 |
+
<div
|
| 280 |
+
data-sidebar="sidebar"
|
| 281 |
+
data-slot="sidebar-inner"
|
| 282 |
+
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
| 283 |
+
>
|
| 284 |
+
{children}
|
| 285 |
+
</div>
|
| 286 |
+
</Highlight>
|
| 287 |
+
</div>
|
| 288 |
+
</div>
|
| 289 |
+
);
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
type SidebarTriggerProps = React.ComponentProps<typeof Button>;
|
| 293 |
+
|
| 294 |
+
function SidebarTrigger({ className, onClick, ...props }: SidebarTriggerProps) {
|
| 295 |
+
const { toggleSidebar } = useSidebar();
|
| 296 |
+
|
| 297 |
+
return (
|
| 298 |
+
<Button
|
| 299 |
+
data-sidebar="trigger"
|
| 300 |
+
data-slot="sidebar-trigger"
|
| 301 |
+
variant="ghost"
|
| 302 |
+
size="icon"
|
| 303 |
+
className={cn('size-7', className)}
|
| 304 |
+
onClick={(event) => {
|
| 305 |
+
onClick?.(event);
|
| 306 |
+
toggleSidebar();
|
| 307 |
+
}}
|
| 308 |
+
{...props}
|
| 309 |
+
>
|
| 310 |
+
<PanelLeftIcon />
|
| 311 |
+
<span className="sr-only">Toggle Sidebar</span>
|
| 312 |
+
</Button>
|
| 313 |
+
);
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
type SidebarRailProps = React.ComponentProps<'button'>;
|
| 317 |
+
|
| 318 |
+
function SidebarRail({ className, ...props }: SidebarRailProps) {
|
| 319 |
+
const { toggleSidebar } = useSidebar();
|
| 320 |
+
|
| 321 |
+
return (
|
| 322 |
+
<button
|
| 323 |
+
data-sidebar="rail"
|
| 324 |
+
data-slot="sidebar-rail"
|
| 325 |
+
aria-label="Toggle Sidebar"
|
| 326 |
+
tabIndex={-1}
|
| 327 |
+
onClick={toggleSidebar}
|
| 328 |
+
title="Toggle Sidebar"
|
| 329 |
+
className={cn(
|
| 330 |
+
'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',
|
| 331 |
+
'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
|
| 332 |
+
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
|
| 333 |
+
'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
|
| 334 |
+
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
|
| 335 |
+
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
|
| 336 |
+
className,
|
| 337 |
+
)}
|
| 338 |
+
{...props}
|
| 339 |
+
/>
|
| 340 |
+
);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
type SidebarInsetProps = React.ComponentProps<'main'>;
|
| 344 |
+
|
| 345 |
+
function SidebarInset({ className, ...props }: SidebarInsetProps) {
|
| 346 |
+
return (
|
| 347 |
+
<main
|
| 348 |
+
data-slot="sidebar-inset"
|
| 349 |
+
className={cn(
|
| 350 |
+
'bg-background relative flex w-full flex-1 flex-col',
|
| 351 |
+
'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',
|
| 352 |
+
className,
|
| 353 |
+
)}
|
| 354 |
+
{...props}
|
| 355 |
+
/>
|
| 356 |
+
);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
type SidebarInputProps = React.ComponentProps<typeof Input>;
|
| 360 |
+
|
| 361 |
+
function SidebarInput({ className, ...props }: SidebarInputProps) {
|
| 362 |
+
return (
|
| 363 |
+
<Input
|
| 364 |
+
data-slot="sidebar-input"
|
| 365 |
+
data-sidebar="input"
|
| 366 |
+
className={cn('bg-background h-8 w-full shadow-none', className)}
|
| 367 |
+
{...props}
|
| 368 |
+
/>
|
| 369 |
+
);
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
type SidebarHeaderProps = React.ComponentProps<'div'>;
|
| 373 |
+
|
| 374 |
+
function SidebarHeader({ className, ...props }: SidebarHeaderProps) {
|
| 375 |
+
return (
|
| 376 |
+
<div
|
| 377 |
+
data-slot="sidebar-header"
|
| 378 |
+
data-sidebar="header"
|
| 379 |
+
className={cn('flex flex-col gap-2 p-2', className)}
|
| 380 |
+
{...props}
|
| 381 |
+
/>
|
| 382 |
+
);
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
type SidebarFooterProps = React.ComponentProps<'div'>;
|
| 386 |
+
|
| 387 |
+
function SidebarFooter({ className, ...props }: SidebarFooterProps) {
|
| 388 |
+
return (
|
| 389 |
+
<div
|
| 390 |
+
data-slot="sidebar-footer"
|
| 391 |
+
data-sidebar="footer"
|
| 392 |
+
className={cn('flex flex-col gap-2 p-2', className)}
|
| 393 |
+
{...props}
|
| 394 |
+
/>
|
| 395 |
+
);
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
type SidebarSeparatorProps = React.ComponentProps<typeof Separator>;
|
| 399 |
+
|
| 400 |
+
function SidebarSeparator({ className, ...props }: SidebarSeparatorProps) {
|
| 401 |
+
return (
|
| 402 |
+
<Separator
|
| 403 |
+
data-slot="sidebar-separator"
|
| 404 |
+
data-sidebar="separator"
|
| 405 |
+
className={cn('bg-sidebar-border mx-2 w-auto', className)}
|
| 406 |
+
{...props}
|
| 407 |
+
/>
|
| 408 |
+
);
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
type SidebarContentProps = React.ComponentProps<'div'>;
|
| 412 |
+
|
| 413 |
+
function SidebarContent({ className, ...props }: SidebarContentProps) {
|
| 414 |
+
return (
|
| 415 |
+
<div
|
| 416 |
+
data-slot="sidebar-content"
|
| 417 |
+
data-sidebar="content"
|
| 418 |
+
className={cn(
|
| 419 |
+
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
|
| 420 |
+
className,
|
| 421 |
+
)}
|
| 422 |
+
{...props}
|
| 423 |
+
/>
|
| 424 |
+
);
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
type SidebarGroupProps = React.ComponentProps<'div'>;
|
| 428 |
+
|
| 429 |
+
function SidebarGroup({ className, ...props }: SidebarGroupProps) {
|
| 430 |
+
return (
|
| 431 |
+
<div
|
| 432 |
+
data-slot="sidebar-group"
|
| 433 |
+
data-sidebar="group"
|
| 434 |
+
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
|
| 435 |
+
{...props}
|
| 436 |
+
/>
|
| 437 |
+
);
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
type SidebarGroupLabelProps = React.ComponentProps<'div'> & {
|
| 441 |
+
asChild?: boolean;
|
| 442 |
+
};
|
| 443 |
+
|
| 444 |
+
function SidebarGroupLabel({
|
| 445 |
+
className,
|
| 446 |
+
asChild = false,
|
| 447 |
+
...props
|
| 448 |
+
}: SidebarGroupLabelProps) {
|
| 449 |
+
const Comp = asChild ? Slot.Root : 'div';
|
| 450 |
+
|
| 451 |
+
return (
|
| 452 |
+
<Comp
|
| 453 |
+
data-slot="sidebar-group-label"
|
| 454 |
+
data-sidebar="group-label"
|
| 455 |
+
className={cn(
|
| 456 |
+
'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-300 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
|
| 457 |
+
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
|
| 458 |
+
className,
|
| 459 |
+
)}
|
| 460 |
+
{...props}
|
| 461 |
+
/>
|
| 462 |
+
);
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
type SidebarGroupActionProps = React.ComponentProps<'button'> & {
|
| 466 |
+
asChild?: boolean;
|
| 467 |
+
};
|
| 468 |
+
|
| 469 |
+
function SidebarGroupAction({
|
| 470 |
+
className,
|
| 471 |
+
asChild = false,
|
| 472 |
+
...props
|
| 473 |
+
}: SidebarGroupActionProps) {
|
| 474 |
+
const Comp = asChild ? Slot.Root : 'button';
|
| 475 |
+
|
| 476 |
+
return (
|
| 477 |
+
<Comp
|
| 478 |
+
data-slot="sidebar-group-action"
|
| 479 |
+
data-sidebar="group-action"
|
| 480 |
+
className={cn(
|
| 481 |
+
'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',
|
| 482 |
+
// Increases the hit area of the button on mobile.
|
| 483 |
+
'after:absolute after:-inset-2 md:after:hidden',
|
| 484 |
+
'group-data-[collapsible=icon]:hidden',
|
| 485 |
+
className,
|
| 486 |
+
)}
|
| 487 |
+
{...props}
|
| 488 |
+
/>
|
| 489 |
+
);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
type SidebarGroupContentProps = React.ComponentProps<'div'>;
|
| 493 |
+
|
| 494 |
+
function SidebarGroupContent({
|
| 495 |
+
className,
|
| 496 |
+
...props
|
| 497 |
+
}: SidebarGroupContentProps) {
|
| 498 |
+
return (
|
| 499 |
+
<div
|
| 500 |
+
data-slot="sidebar-group-content"
|
| 501 |
+
data-sidebar="group-content"
|
| 502 |
+
className={cn('w-full text-sm', className)}
|
| 503 |
+
{...props}
|
| 504 |
+
/>
|
| 505 |
+
);
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
type SidebarMenuProps = React.ComponentProps<'ul'>;
|
| 509 |
+
|
| 510 |
+
function SidebarMenu({ className, ...props }: SidebarMenuProps) {
|
| 511 |
+
return (
|
| 512 |
+
<ul
|
| 513 |
+
data-slot="sidebar-menu"
|
| 514 |
+
data-sidebar="menu"
|
| 515 |
+
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
|
| 516 |
+
{...props}
|
| 517 |
+
/>
|
| 518 |
+
);
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
type SidebarMenuItemProps = React.ComponentProps<'li'>;
|
| 522 |
+
|
| 523 |
+
function SidebarMenuItem({ className, ...props }: SidebarMenuItemProps) {
|
| 524 |
+
return (
|
| 525 |
+
<li
|
| 526 |
+
data-slot="sidebar-menu-item"
|
| 527 |
+
data-sidebar="menu-item"
|
| 528 |
+
className={cn('group/menu-item relative', className)}
|
| 529 |
+
{...props}
|
| 530 |
+
/>
|
| 531 |
+
);
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
const sidebarMenuButtonActiveVariants = cva(
|
| 535 |
+
'bg-sidebar-accent text-sidebar-accent-foreground rounded-md',
|
| 536 |
+
{
|
| 537 |
+
variants: {
|
| 538 |
+
variant: {
|
| 539 |
+
default: 'bg-sidebar-accent text-sidebar-accent-foreground',
|
| 540 |
+
outline:
|
| 541 |
+
'bg-sidebar-accent text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
|
| 542 |
+
},
|
| 543 |
+
},
|
| 544 |
+
defaultVariants: {
|
| 545 |
+
variant: 'default',
|
| 546 |
+
},
|
| 547 |
+
},
|
| 548 |
+
);
|
| 549 |
+
|
| 550 |
+
const sidebarMenuButtonVariants = cva(
|
| 551 |
+
'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] [&:not([data-highlight])]:hover:bg-sidebar-accent [&:not([data-highlight])]: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 [&:not([data-highlight])]:data-[state=open]:hover:bg-sidebar-accent [&:not([data-highlight])]: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',
|
| 552 |
+
{
|
| 553 |
+
variants: {
|
| 554 |
+
variant: {
|
| 555 |
+
default:
|
| 556 |
+
'[&:not([data-highlight])]:hover:bg-sidebar-accent [&:not([data-highlight])]:hover:text-sidebar-accent-foreground',
|
| 557 |
+
outline:
|
| 558 |
+
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] [&:not([data-highlight])]:hover:bg-sidebar-accent [&:not([data-highlight])]:hover:text-sidebar-accent-foreground [&:not([data-highlight])]:hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
|
| 559 |
+
},
|
| 560 |
+
size: {
|
| 561 |
+
default: 'h-8 text-sm',
|
| 562 |
+
sm: 'h-7 text-xs',
|
| 563 |
+
lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!',
|
| 564 |
+
},
|
| 565 |
+
},
|
| 566 |
+
defaultVariants: {
|
| 567 |
+
variant: 'default',
|
| 568 |
+
size: 'default',
|
| 569 |
+
},
|
| 570 |
+
},
|
| 571 |
+
);
|
| 572 |
+
|
| 573 |
+
type SidebarMenuButtonProps = React.ComponentProps<'button'> & {
|
| 574 |
+
asChild?: boolean;
|
| 575 |
+
isActive?: boolean;
|
| 576 |
+
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
| 577 |
+
} & VariantProps<typeof sidebarMenuButtonVariants>;
|
| 578 |
+
|
| 579 |
+
function SidebarMenuButton({
|
| 580 |
+
asChild = false,
|
| 581 |
+
isActive = false,
|
| 582 |
+
variant = 'default',
|
| 583 |
+
size = 'default',
|
| 584 |
+
tooltip,
|
| 585 |
+
className,
|
| 586 |
+
...props
|
| 587 |
+
}: SidebarMenuButtonProps) {
|
| 588 |
+
const Comp = asChild ? Slot.Root : 'button';
|
| 589 |
+
const { isMobile, state } = useSidebar();
|
| 590 |
+
|
| 591 |
+
const button = (
|
| 592 |
+
<HighlightItem
|
| 593 |
+
activeClassName={sidebarMenuButtonActiveVariants({ variant })}
|
| 594 |
+
>
|
| 595 |
+
<Comp
|
| 596 |
+
data-slot="sidebar-menu-button"
|
| 597 |
+
data-sidebar="menu-button"
|
| 598 |
+
data-size={size}
|
| 599 |
+
data-active={isActive}
|
| 600 |
+
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
| 601 |
+
{...props}
|
| 602 |
+
/>
|
| 603 |
+
</HighlightItem>
|
| 604 |
+
);
|
| 605 |
+
|
| 606 |
+
if (!tooltip) {
|
| 607 |
+
return button;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
if (typeof tooltip === 'string') {
|
| 611 |
+
tooltip = {
|
| 612 |
+
children: tooltip,
|
| 613 |
+
};
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
return (
|
| 617 |
+
<Tooltip side="right" align="center">
|
| 618 |
+
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
| 619 |
+
<TooltipContent hidden={state !== 'collapsed' || isMobile} {...tooltip} />
|
| 620 |
+
</Tooltip>
|
| 621 |
+
);
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
type SidebarMenuActionProps = React.ComponentProps<'button'> & {
|
| 625 |
+
asChild?: boolean;
|
| 626 |
+
showOnHover?: boolean;
|
| 627 |
+
};
|
| 628 |
+
|
| 629 |
+
function SidebarMenuAction({
|
| 630 |
+
className,
|
| 631 |
+
asChild = false,
|
| 632 |
+
showOnHover = false,
|
| 633 |
+
...props
|
| 634 |
+
}: SidebarMenuActionProps) {
|
| 635 |
+
const Comp = asChild ? Slot.Root : 'button';
|
| 636 |
+
|
| 637 |
+
return (
|
| 638 |
+
<Comp
|
| 639 |
+
data-slot="sidebar-menu-action"
|
| 640 |
+
data-sidebar="menu-action"
|
| 641 |
+
className={cn(
|
| 642 |
+
// Increases the hit area of the button on mobile.
|
| 643 |
+
'z-[1] 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',
|
| 644 |
+
'after:absolute after:-inset-2 md:after:hidden',
|
| 645 |
+
'peer-data-[size=sm]/menu-button:top-1',
|
| 646 |
+
'peer-data-[size=default]/menu-button:top-1.5',
|
| 647 |
+
'peer-data-[size=lg]/menu-button:top-2.5',
|
| 648 |
+
'group-data-[collapsible=icon]:hidden',
|
| 649 |
+
showOnHover &&
|
| 650 |
+
'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',
|
| 651 |
+
className,
|
| 652 |
+
)}
|
| 653 |
+
{...props}
|
| 654 |
+
/>
|
| 655 |
+
);
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
type SidebarMenuBadgeProps = React.ComponentProps<'div'>;
|
| 659 |
+
|
| 660 |
+
function SidebarMenuBadge({ className, ...props }: SidebarMenuBadgeProps) {
|
| 661 |
+
return (
|
| 662 |
+
<div
|
| 663 |
+
data-slot="sidebar-menu-badge"
|
| 664 |
+
data-sidebar="menu-badge"
|
| 665 |
+
className={cn(
|
| 666 |
+
'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',
|
| 667 |
+
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
|
| 668 |
+
'peer-data-[size=sm]/menu-button:top-1',
|
| 669 |
+
'peer-data-[size=default]/menu-button:top-1.5',
|
| 670 |
+
'peer-data-[size=lg]/menu-button:top-2.5',
|
| 671 |
+
'group-data-[collapsible=icon]:hidden',
|
| 672 |
+
className,
|
| 673 |
+
)}
|
| 674 |
+
{...props}
|
| 675 |
+
/>
|
| 676 |
+
);
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
type SidebarMenuSkeletonProps = React.ComponentProps<'div'> & {
|
| 680 |
+
showIcon?: boolean;
|
| 681 |
+
};
|
| 682 |
+
|
| 683 |
+
function SidebarMenuSkeleton({
|
| 684 |
+
className,
|
| 685 |
+
showIcon = false,
|
| 686 |
+
...props
|
| 687 |
+
}: SidebarMenuSkeletonProps) {
|
| 688 |
+
// Random width between 50 to 90%.
|
| 689 |
+
const width = React.useMemo(() => {
|
| 690 |
+
return `${Math.floor(Math.random() * 40) + 50}%`;
|
| 691 |
+
}, []);
|
| 692 |
+
|
| 693 |
+
return (
|
| 694 |
+
<div
|
| 695 |
+
data-slot="sidebar-menu-skeleton"
|
| 696 |
+
data-sidebar="menu-skeleton"
|
| 697 |
+
className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
|
| 698 |
+
{...props}
|
| 699 |
+
>
|
| 700 |
+
{showIcon && (
|
| 701 |
+
<Skeleton
|
| 702 |
+
className="size-4 rounded-md"
|
| 703 |
+
data-sidebar="menu-skeleton-icon"
|
| 704 |
+
/>
|
| 705 |
+
)}
|
| 706 |
+
<Skeleton
|
| 707 |
+
className="h-4 max-w-(--skeleton-width) flex-1"
|
| 708 |
+
data-sidebar="menu-skeleton-text"
|
| 709 |
+
style={
|
| 710 |
+
{
|
| 711 |
+
'--skeleton-width': width,
|
| 712 |
+
} as React.CSSProperties
|
| 713 |
+
}
|
| 714 |
+
/>
|
| 715 |
+
</div>
|
| 716 |
+
);
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
type SidebarMenuSubProps = React.ComponentProps<'ul'>;
|
| 720 |
+
|
| 721 |
+
function SidebarMenuSub({ className, ...props }: SidebarMenuSubProps) {
|
| 722 |
+
return (
|
| 723 |
+
<ul
|
| 724 |
+
data-slot="sidebar-menu-sub"
|
| 725 |
+
data-sidebar="menu-sub"
|
| 726 |
+
className={cn(
|
| 727 |
+
'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',
|
| 728 |
+
'group-data-[collapsible=icon]:hidden',
|
| 729 |
+
className,
|
| 730 |
+
)}
|
| 731 |
+
{...props}
|
| 732 |
+
/>
|
| 733 |
+
);
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
type SidebarMenuSubItemProps = React.ComponentProps<'li'>;
|
| 737 |
+
|
| 738 |
+
function SidebarMenuSubItem({ className, ...props }: SidebarMenuSubItemProps) {
|
| 739 |
+
return (
|
| 740 |
+
<li
|
| 741 |
+
data-slot="sidebar-menu-sub-item"
|
| 742 |
+
data-sidebar="menu-sub-item"
|
| 743 |
+
className={cn('group/menu-sub-item relative', className)}
|
| 744 |
+
{...props}
|
| 745 |
+
/>
|
| 746 |
+
);
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
type SidebarMenuSubButtonProps = React.ComponentProps<'a'> & {
|
| 750 |
+
asChild?: boolean;
|
| 751 |
+
size?: 'sm' | 'md';
|
| 752 |
+
isActive?: boolean;
|
| 753 |
+
};
|
| 754 |
+
|
| 755 |
+
function SidebarMenuSubButton({
|
| 756 |
+
asChild = false,
|
| 757 |
+
size = 'md',
|
| 758 |
+
isActive = false,
|
| 759 |
+
className,
|
| 760 |
+
...props
|
| 761 |
+
}: SidebarMenuSubButtonProps) {
|
| 762 |
+
const Comp = asChild ? Slot.Root : 'a';
|
| 763 |
+
|
| 764 |
+
return (
|
| 765 |
+
<HighlightItem activeClassName="bg-sidebar-accent text-sidebar-accent-foreground rounded-md">
|
| 766 |
+
<Comp
|
| 767 |
+
data-slot="sidebar-menu-sub-button"
|
| 768 |
+
data-sidebar="menu-sub-button"
|
| 769 |
+
data-size={size}
|
| 770 |
+
data-active={isActive}
|
| 771 |
+
className={cn(
|
| 772 |
+
'text-sidebar-foreground ring-sidebar-ring [&:not([data-highlight])]:hover:bg-sidebar-accent [&:not([data-highlight])]: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',
|
| 773 |
+
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
|
| 774 |
+
size === 'sm' && 'text-xs',
|
| 775 |
+
size === 'md' && 'text-sm',
|
| 776 |
+
'group-data-[collapsible=icon]:hidden',
|
| 777 |
+
className,
|
| 778 |
+
)}
|
| 779 |
+
{...props}
|
| 780 |
+
/>
|
| 781 |
+
</HighlightItem>
|
| 782 |
+
);
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
export {
|
| 786 |
+
Sidebar,
|
| 787 |
+
SidebarContent,
|
| 788 |
+
SidebarFooter,
|
| 789 |
+
SidebarGroup,
|
| 790 |
+
SidebarGroupAction,
|
| 791 |
+
SidebarGroupContent,
|
| 792 |
+
SidebarGroupLabel,
|
| 793 |
+
SidebarHeader,
|
| 794 |
+
SidebarInput,
|
| 795 |
+
SidebarInset,
|
| 796 |
+
SidebarMenu,
|
| 797 |
+
SidebarMenuAction,
|
| 798 |
+
SidebarMenuBadge,
|
| 799 |
+
SidebarMenuButton,
|
| 800 |
+
SidebarMenuItem,
|
| 801 |
+
SidebarMenuSkeleton,
|
| 802 |
+
SidebarMenuSub,
|
| 803 |
+
SidebarMenuSubButton,
|
| 804 |
+
SidebarMenuSubItem,
|
| 805 |
+
SidebarProvider,
|
| 806 |
+
SidebarRail,
|
| 807 |
+
SidebarSeparator,
|
| 808 |
+
SidebarTrigger,
|
| 809 |
+
useSidebar,
|
| 810 |
+
};
|
ui/components/animate-ui/primitives/animate/slot.tsx
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import * as React from 'react';
|
| 4 |
+
import { motion, isMotionComponent, type HTMLMotionProps } from 'motion/react';
|
| 5 |
+
import { cn } from '@/lib/utils';
|
| 6 |
+
|
| 7 |
+
type AnyProps = Record<string, unknown>;
|
| 8 |
+
|
| 9 |
+
type DOMMotionProps<T extends HTMLElement = HTMLElement> = Omit<
|
| 10 |
+
HTMLMotionProps<keyof HTMLElementTagNameMap>,
|
| 11 |
+
'ref'
|
| 12 |
+
> & { ref?: React.Ref<T> };
|
| 13 |
+
|
| 14 |
+
type WithAsChild<Base extends object> =
|
| 15 |
+
| (Base & { asChild: true; children: React.ReactElement })
|
| 16 |
+
| (Base & { asChild?: false | undefined });
|
| 17 |
+
|
| 18 |
+
type SlotProps<T extends HTMLElement = HTMLElement> = {
|
| 19 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 20 |
+
children?: any;
|
| 21 |
+
} & DOMMotionProps<T>;
|
| 22 |
+
|
| 23 |
+
function mergeRefs<T>(
|
| 24 |
+
...refs: (React.Ref<T> | undefined)[]
|
| 25 |
+
): React.RefCallback<T> {
|
| 26 |
+
return (node) => {
|
| 27 |
+
refs.forEach((ref) => {
|
| 28 |
+
if (!ref) return;
|
| 29 |
+
if (typeof ref === 'function') {
|
| 30 |
+
ref(node);
|
| 31 |
+
} else {
|
| 32 |
+
(ref as React.RefObject<T | null>).current = node;
|
| 33 |
+
}
|
| 34 |
+
});
|
| 35 |
+
};
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
function mergeProps<T extends HTMLElement>(
|
| 39 |
+
childProps: AnyProps,
|
| 40 |
+
slotProps: DOMMotionProps<T>,
|
| 41 |
+
): AnyProps {
|
| 42 |
+
const merged: AnyProps = { ...childProps, ...slotProps };
|
| 43 |
+
|
| 44 |
+
if (childProps.className || slotProps.className) {
|
| 45 |
+
merged.className = cn(
|
| 46 |
+
childProps.className as string,
|
| 47 |
+
slotProps.className as string,
|
| 48 |
+
);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
if (childProps.style || slotProps.style) {
|
| 52 |
+
merged.style = {
|
| 53 |
+
...(childProps.style as React.CSSProperties),
|
| 54 |
+
...(slotProps.style as React.CSSProperties),
|
| 55 |
+
};
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
return merged;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function Slot<T extends HTMLElement = HTMLElement>({
|
| 62 |
+
children,
|
| 63 |
+
ref,
|
| 64 |
+
...props
|
| 65 |
+
}: SlotProps<T>) {
|
| 66 |
+
const isAlreadyMotion =
|
| 67 |
+
typeof children.type === 'object' &&
|
| 68 |
+
children.type !== null &&
|
| 69 |
+
isMotionComponent(children.type);
|
| 70 |
+
|
| 71 |
+
const Base = React.useMemo(
|
| 72 |
+
() =>
|
| 73 |
+
isAlreadyMotion
|
| 74 |
+
? (children.type as React.ElementType)
|
| 75 |
+
: motion.create(children.type as React.ElementType),
|
| 76 |
+
[isAlreadyMotion, children.type],
|
| 77 |
+
);
|
| 78 |
+
|
| 79 |
+
if (!React.isValidElement(children)) return null;
|
| 80 |
+
|
| 81 |
+
const { ref: childRef, ...childProps } = children.props as AnyProps;
|
| 82 |
+
|
| 83 |
+
const mergedProps = mergeProps(childProps, props);
|
| 84 |
+
|
| 85 |
+
return (
|
| 86 |
+
<Base {...mergedProps} ref={mergeRefs(childRef as React.Ref<T>, ref)} />
|
| 87 |
+
);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
export {
|
| 91 |
+
Slot,
|
| 92 |
+
type SlotProps,
|
| 93 |
+
type WithAsChild,
|
| 94 |
+
type DOMMotionProps,
|
| 95 |
+
type AnyProps,
|
| 96 |
+
};
|
ui/components/animate-ui/primitives/animate/tooltip.tsx
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import * as React from 'react';
|
| 4 |
+
import {
|
| 5 |
+
motion,
|
| 6 |
+
AnimatePresence,
|
| 7 |
+
LayoutGroup,
|
| 8 |
+
type Transition,
|
| 9 |
+
type HTMLMotionProps,
|
| 10 |
+
} from 'motion/react';
|
| 11 |
+
import {
|
| 12 |
+
useFloating,
|
| 13 |
+
autoUpdate,
|
| 14 |
+
offset as floatingOffset,
|
| 15 |
+
flip,
|
| 16 |
+
shift,
|
| 17 |
+
arrow as floatingArrow,
|
| 18 |
+
FloatingPortal,
|
| 19 |
+
FloatingArrow,
|
| 20 |
+
type UseFloatingReturn,
|
| 21 |
+
} from '@floating-ui/react';
|
| 22 |
+
|
| 23 |
+
import { getStrictContext } from '@/lib/get-strict-context';
|
| 24 |
+
import { Slot, type WithAsChild } from '@/components/animate-ui/primitives/animate/slot';
|
| 25 |
+
|
| 26 |
+
type Side = 'top' | 'bottom' | 'left' | 'right';
|
| 27 |
+
type Align = 'start' | 'center' | 'end';
|
| 28 |
+
|
| 29 |
+
type TooltipData = {
|
| 30 |
+
contentProps: HTMLMotionProps<'div'>;
|
| 31 |
+
contentAsChild: boolean;
|
| 32 |
+
rect: DOMRect;
|
| 33 |
+
side: Side;
|
| 34 |
+
sideOffset: number;
|
| 35 |
+
align: Align;
|
| 36 |
+
alignOffset: number;
|
| 37 |
+
id: string;
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
type GlobalTooltipContextType = {
|
| 41 |
+
showTooltip: (data: TooltipData) => void;
|
| 42 |
+
hideTooltip: () => void;
|
| 43 |
+
hideImmediate: () => void;
|
| 44 |
+
currentTooltip: TooltipData | null;
|
| 45 |
+
transition: Transition;
|
| 46 |
+
globalId: string;
|
| 47 |
+
setReferenceEl: (el: HTMLElement | null) => void;
|
| 48 |
+
referenceElRef: React.RefObject<HTMLElement | null>;
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
const [GlobalTooltipProvider, useGlobalTooltip] =
|
| 52 |
+
getStrictContext<GlobalTooltipContextType>('GlobalTooltipProvider');
|
| 53 |
+
|
| 54 |
+
type TooltipContextType = {
|
| 55 |
+
props: HTMLMotionProps<'div'>;
|
| 56 |
+
setProps: React.Dispatch<React.SetStateAction<HTMLMotionProps<'div'>>>;
|
| 57 |
+
asChild: boolean;
|
| 58 |
+
setAsChild: React.Dispatch<React.SetStateAction<boolean>>;
|
| 59 |
+
side: Side;
|
| 60 |
+
sideOffset: number;
|
| 61 |
+
align: Align;
|
| 62 |
+
alignOffset: number;
|
| 63 |
+
id: string;
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
const [LocalTooltipProvider, useTooltip] = getStrictContext<TooltipContextType>(
|
| 67 |
+
'LocalTooltipProvider',
|
| 68 |
+
);
|
| 69 |
+
|
| 70 |
+
type TooltipPosition = { x: number; y: number };
|
| 71 |
+
|
| 72 |
+
function getResolvedSide(placement: Side | `${Side}-${Align}`) {
|
| 73 |
+
if (placement.includes('-')) {
|
| 74 |
+
return placement.split('-')[0] as Side;
|
| 75 |
+
}
|
| 76 |
+
return placement as Side;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function initialFromSide(side: Side): Partial<Record<'x' | 'y', number>> {
|
| 80 |
+
if (side === 'top') return { y: 15 };
|
| 81 |
+
if (side === 'bottom') return { y: -15 };
|
| 82 |
+
if (side === 'left') return { x: 15 };
|
| 83 |
+
return { x: -15 };
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
type TooltipProviderProps = {
|
| 87 |
+
children: React.ReactNode;
|
| 88 |
+
id?: string;
|
| 89 |
+
openDelay?: number;
|
| 90 |
+
closeDelay?: number;
|
| 91 |
+
transition?: Transition;
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
function TooltipProvider({
|
| 95 |
+
children,
|
| 96 |
+
id,
|
| 97 |
+
openDelay = 700,
|
| 98 |
+
closeDelay = 300,
|
| 99 |
+
transition = { type: 'spring', stiffness: 300, damping: 35 },
|
| 100 |
+
}: TooltipProviderProps) {
|
| 101 |
+
const globalId = React.useId();
|
| 102 |
+
const [currentTooltip, setCurrentTooltip] =
|
| 103 |
+
React.useState<TooltipData | null>(null);
|
| 104 |
+
const timeoutRef = React.useRef<number | null>(null);
|
| 105 |
+
const lastCloseTimeRef = React.useRef<number>(0);
|
| 106 |
+
const referenceElRef = React.useRef<HTMLElement | null>(null);
|
| 107 |
+
|
| 108 |
+
const showTooltip = React.useCallback(
|
| 109 |
+
(data: TooltipData) => {
|
| 110 |
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
| 111 |
+
if (currentTooltip !== null) {
|
| 112 |
+
setCurrentTooltip(data);
|
| 113 |
+
return;
|
| 114 |
+
}
|
| 115 |
+
const now = Date.now();
|
| 116 |
+
const delay = now - lastCloseTimeRef.current < closeDelay ? 0 : openDelay;
|
| 117 |
+
timeoutRef.current = window.setTimeout(
|
| 118 |
+
() => setCurrentTooltip(data),
|
| 119 |
+
delay,
|
| 120 |
+
);
|
| 121 |
+
},
|
| 122 |
+
[openDelay, closeDelay, currentTooltip],
|
| 123 |
+
);
|
| 124 |
+
|
| 125 |
+
const hideTooltip = React.useCallback(() => {
|
| 126 |
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
| 127 |
+
timeoutRef.current = window.setTimeout(() => {
|
| 128 |
+
setCurrentTooltip(null);
|
| 129 |
+
lastCloseTimeRef.current = Date.now();
|
| 130 |
+
}, closeDelay);
|
| 131 |
+
}, [closeDelay]);
|
| 132 |
+
|
| 133 |
+
const hideImmediate = React.useCallback(() => {
|
| 134 |
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
| 135 |
+
setCurrentTooltip(null);
|
| 136 |
+
lastCloseTimeRef.current = Date.now();
|
| 137 |
+
}, []);
|
| 138 |
+
|
| 139 |
+
const setReferenceEl = React.useCallback((el: HTMLElement | null) => {
|
| 140 |
+
referenceElRef.current = el;
|
| 141 |
+
}, []);
|
| 142 |
+
|
| 143 |
+
React.useEffect(() => {
|
| 144 |
+
const onKeyDown = (e: KeyboardEvent) => {
|
| 145 |
+
if (e.key === 'Escape') hideImmediate();
|
| 146 |
+
};
|
| 147 |
+
window.addEventListener('keydown', onKeyDown, true);
|
| 148 |
+
window.addEventListener('scroll', hideImmediate, true);
|
| 149 |
+
window.addEventListener('resize', hideImmediate, true);
|
| 150 |
+
return () => {
|
| 151 |
+
window.removeEventListener('keydown', onKeyDown, true);
|
| 152 |
+
window.removeEventListener('scroll', hideImmediate, true);
|
| 153 |
+
window.removeEventListener('resize', hideImmediate, true);
|
| 154 |
+
};
|
| 155 |
+
}, [hideImmediate]);
|
| 156 |
+
|
| 157 |
+
return (
|
| 158 |
+
<GlobalTooltipProvider
|
| 159 |
+
value={{
|
| 160 |
+
showTooltip,
|
| 161 |
+
hideTooltip,
|
| 162 |
+
hideImmediate,
|
| 163 |
+
currentTooltip,
|
| 164 |
+
transition,
|
| 165 |
+
globalId: id ?? globalId,
|
| 166 |
+
setReferenceEl,
|
| 167 |
+
referenceElRef,
|
| 168 |
+
}}
|
| 169 |
+
>
|
| 170 |
+
<LayoutGroup>{children}</LayoutGroup>
|
| 171 |
+
<TooltipOverlay />
|
| 172 |
+
</GlobalTooltipProvider>
|
| 173 |
+
);
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
type RenderedTooltipContextType = {
|
| 177 |
+
side: Side;
|
| 178 |
+
align: Align;
|
| 179 |
+
open: boolean;
|
| 180 |
+
};
|
| 181 |
+
|
| 182 |
+
const [RenderedTooltipProvider, useRenderedTooltip] =
|
| 183 |
+
getStrictContext<RenderedTooltipContextType>('RenderedTooltipContext');
|
| 184 |
+
|
| 185 |
+
type FloatingContextType = {
|
| 186 |
+
context: UseFloatingReturn['context'];
|
| 187 |
+
arrowRef: React.RefObject<SVGSVGElement | null>;
|
| 188 |
+
};
|
| 189 |
+
|
| 190 |
+
const [FloatingProvider, useFloatingContext] =
|
| 191 |
+
getStrictContext<FloatingContextType>('FloatingContext');
|
| 192 |
+
|
| 193 |
+
const MotionTooltipArrow = motion.create(FloatingArrow);
|
| 194 |
+
|
| 195 |
+
type TooltipArrowProps = Omit<
|
| 196 |
+
React.ComponentProps<typeof MotionTooltipArrow>,
|
| 197 |
+
'context'
|
| 198 |
+
> & {
|
| 199 |
+
withTransition?: boolean;
|
| 200 |
+
};
|
| 201 |
+
|
| 202 |
+
function TooltipArrow({
|
| 203 |
+
ref,
|
| 204 |
+
withTransition = true,
|
| 205 |
+
...props
|
| 206 |
+
}: TooltipArrowProps) {
|
| 207 |
+
const { side, align, open } = useRenderedTooltip();
|
| 208 |
+
const { context, arrowRef } = useFloatingContext();
|
| 209 |
+
const { transition, globalId } = useGlobalTooltip();
|
| 210 |
+
React.useImperativeHandle(ref, () => arrowRef.current as SVGSVGElement);
|
| 211 |
+
|
| 212 |
+
const deg = { top: 0, right: 90, bottom: 180, left: -90 }[side];
|
| 213 |
+
|
| 214 |
+
return (
|
| 215 |
+
<MotionTooltipArrow
|
| 216 |
+
ref={arrowRef}
|
| 217 |
+
context={context}
|
| 218 |
+
data-state={open ? 'open' : 'closed'}
|
| 219 |
+
data-side={side}
|
| 220 |
+
data-align={align}
|
| 221 |
+
data-slot="tooltip-arrow"
|
| 222 |
+
style={{ rotate: deg }}
|
| 223 |
+
layoutId={withTransition ? `tooltip-arrow-${globalId}` : undefined}
|
| 224 |
+
transition={withTransition ? transition : undefined}
|
| 225 |
+
{...props}
|
| 226 |
+
/>
|
| 227 |
+
);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
type TooltipPortalProps = React.ComponentProps<typeof FloatingPortal>;
|
| 231 |
+
|
| 232 |
+
function TooltipPortal(props: TooltipPortalProps) {
|
| 233 |
+
return <FloatingPortal {...props} />;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
function TooltipOverlay() {
|
| 237 |
+
const { currentTooltip, transition, globalId, referenceElRef } =
|
| 238 |
+
useGlobalTooltip();
|
| 239 |
+
|
| 240 |
+
const [rendered, setRendered] = React.useState<{
|
| 241 |
+
data: TooltipData | null;
|
| 242 |
+
open: boolean;
|
| 243 |
+
}>({ data: null, open: false });
|
| 244 |
+
|
| 245 |
+
const arrowRef = React.useRef<SVGSVGElement | null>(null);
|
| 246 |
+
|
| 247 |
+
const side = rendered.data?.side ?? 'top';
|
| 248 |
+
const align = rendered.data?.align ?? 'center';
|
| 249 |
+
|
| 250 |
+
const { refs, x, y, strategy, context, update } = useFloating({
|
| 251 |
+
placement: align === 'center' ? side : `${side}-${align}`,
|
| 252 |
+
whileElementsMounted: autoUpdate,
|
| 253 |
+
middleware: [
|
| 254 |
+
floatingOffset({
|
| 255 |
+
mainAxis: rendered.data?.sideOffset ?? 0,
|
| 256 |
+
crossAxis: rendered.data?.alignOffset ?? 0,
|
| 257 |
+
}),
|
| 258 |
+
flip(),
|
| 259 |
+
shift({ padding: 8 }),
|
| 260 |
+
floatingArrow({ element: arrowRef }),
|
| 261 |
+
],
|
| 262 |
+
});
|
| 263 |
+
|
| 264 |
+
React.useEffect(() => {
|
| 265 |
+
if (currentTooltip) {
|
| 266 |
+
setRendered({ data: currentTooltip, open: true });
|
| 267 |
+
} else {
|
| 268 |
+
setRendered((p) => (p.data ? { ...p, open: false } : p));
|
| 269 |
+
}
|
| 270 |
+
}, [currentTooltip]);
|
| 271 |
+
|
| 272 |
+
React.useLayoutEffect(() => {
|
| 273 |
+
if (referenceElRef.current) {
|
| 274 |
+
refs.setReference(referenceElRef.current);
|
| 275 |
+
update();
|
| 276 |
+
}
|
| 277 |
+
}, [referenceElRef, refs, update, rendered.data]);
|
| 278 |
+
|
| 279 |
+
const ready = x != null && y != null;
|
| 280 |
+
const Component = rendered.data?.contentAsChild ? Slot : motion.div;
|
| 281 |
+
const resolvedSide = getResolvedSide(context.placement);
|
| 282 |
+
|
| 283 |
+
return (
|
| 284 |
+
<AnimatePresence mode="wait">
|
| 285 |
+
{rendered.data && ready && (
|
| 286 |
+
<TooltipPortal>
|
| 287 |
+
<div
|
| 288 |
+
ref={refs.setFloating}
|
| 289 |
+
data-slot="tooltip-overlay"
|
| 290 |
+
data-side={resolvedSide}
|
| 291 |
+
data-align={rendered.data.align}
|
| 292 |
+
data-state={rendered.open ? 'open' : 'closed'}
|
| 293 |
+
style={{
|
| 294 |
+
position: strategy,
|
| 295 |
+
top: 0,
|
| 296 |
+
left: 0,
|
| 297 |
+
zIndex: 50,
|
| 298 |
+
transform: `translate3d(${x!}px, ${y!}px, 0)`,
|
| 299 |
+
}}
|
| 300 |
+
>
|
| 301 |
+
<FloatingProvider value={{ context, arrowRef }}>
|
| 302 |
+
<RenderedTooltipProvider
|
| 303 |
+
value={{
|
| 304 |
+
side: resolvedSide,
|
| 305 |
+
align: rendered.data.align,
|
| 306 |
+
open: rendered.open,
|
| 307 |
+
}}
|
| 308 |
+
>
|
| 309 |
+
<Component
|
| 310 |
+
data-slot="tooltip-content"
|
| 311 |
+
data-side={resolvedSide}
|
| 312 |
+
data-align={rendered.data.align}
|
| 313 |
+
data-state={rendered.open ? 'open' : 'closed'}
|
| 314 |
+
layoutId={`tooltip-content-${globalId}`}
|
| 315 |
+
initial={{
|
| 316 |
+
opacity: 0,
|
| 317 |
+
scale: 0,
|
| 318 |
+
...initialFromSide(rendered.data.side),
|
| 319 |
+
}}
|
| 320 |
+
animate={
|
| 321 |
+
rendered.open
|
| 322 |
+
? { opacity: 1, scale: 1, x: 0, y: 0 }
|
| 323 |
+
: {
|
| 324 |
+
opacity: 0,
|
| 325 |
+
scale: 0,
|
| 326 |
+
...initialFromSide(rendered.data.side),
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
exit={{
|
| 330 |
+
opacity: 0,
|
| 331 |
+
scale: 0,
|
| 332 |
+
...initialFromSide(rendered.data.side),
|
| 333 |
+
}}
|
| 334 |
+
onAnimationComplete={() => {
|
| 335 |
+
if (!rendered.open)
|
| 336 |
+
setRendered({ data: null, open: false });
|
| 337 |
+
}}
|
| 338 |
+
transition={transition}
|
| 339 |
+
{...rendered.data.contentProps}
|
| 340 |
+
style={{
|
| 341 |
+
position: 'relative',
|
| 342 |
+
...(rendered.data.contentProps?.style || {}),
|
| 343 |
+
}}
|
| 344 |
+
/>
|
| 345 |
+
</RenderedTooltipProvider>
|
| 346 |
+
</FloatingProvider>
|
| 347 |
+
</div>
|
| 348 |
+
</TooltipPortal>
|
| 349 |
+
)}
|
| 350 |
+
</AnimatePresence>
|
| 351 |
+
);
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
type TooltipProps = {
|
| 355 |
+
children: React.ReactNode;
|
| 356 |
+
side?: Side;
|
| 357 |
+
sideOffset?: number;
|
| 358 |
+
align?: Align;
|
| 359 |
+
alignOffset?: number;
|
| 360 |
+
};
|
| 361 |
+
|
| 362 |
+
function Tooltip({
|
| 363 |
+
children,
|
| 364 |
+
side = 'top',
|
| 365 |
+
sideOffset = 0,
|
| 366 |
+
align = 'center',
|
| 367 |
+
alignOffset = 0,
|
| 368 |
+
}: TooltipProps) {
|
| 369 |
+
const id = React.useId();
|
| 370 |
+
const [props, setProps] = React.useState<HTMLMotionProps<'div'>>({});
|
| 371 |
+
const [asChild, setAsChild] = React.useState(false);
|
| 372 |
+
|
| 373 |
+
return (
|
| 374 |
+
<LocalTooltipProvider
|
| 375 |
+
value={{
|
| 376 |
+
props,
|
| 377 |
+
setProps,
|
| 378 |
+
asChild,
|
| 379 |
+
setAsChild,
|
| 380 |
+
side,
|
| 381 |
+
sideOffset,
|
| 382 |
+
align,
|
| 383 |
+
alignOffset,
|
| 384 |
+
id,
|
| 385 |
+
}}
|
| 386 |
+
>
|
| 387 |
+
{children}
|
| 388 |
+
</LocalTooltipProvider>
|
| 389 |
+
);
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
type TooltipContentProps = WithAsChild<HTMLMotionProps<'div'>>;
|
| 393 |
+
|
| 394 |
+
function shallowEqualWithoutChildren(
|
| 395 |
+
a?: HTMLMotionProps<'div'>,
|
| 396 |
+
b?: HTMLMotionProps<'div'>,
|
| 397 |
+
) {
|
| 398 |
+
if (a === b) return true;
|
| 399 |
+
if (!a || !b) return false;
|
| 400 |
+
const keysA = Object.keys(a).filter((k) => k !== 'children');
|
| 401 |
+
const keysB = Object.keys(b).filter((k) => k !== 'children');
|
| 402 |
+
if (keysA.length !== keysB.length) return false;
|
| 403 |
+
for (const k of keysA) {
|
| 404 |
+
// @ts-expect-error index
|
| 405 |
+
if (a[k] !== b[k]) return false;
|
| 406 |
+
}
|
| 407 |
+
return true;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
function TooltipContent({ asChild = false, ...props }: TooltipContentProps) {
|
| 411 |
+
const { setProps, setAsChild } = useTooltip();
|
| 412 |
+
const lastPropsRef = React.useRef<HTMLMotionProps<'div'> | undefined>(
|
| 413 |
+
undefined,
|
| 414 |
+
);
|
| 415 |
+
|
| 416 |
+
React.useEffect(() => {
|
| 417 |
+
if (!shallowEqualWithoutChildren(lastPropsRef.current, props)) {
|
| 418 |
+
lastPropsRef.current = props;
|
| 419 |
+
setProps(props);
|
| 420 |
+
}
|
| 421 |
+
}, [props, setProps]);
|
| 422 |
+
|
| 423 |
+
React.useEffect(() => {
|
| 424 |
+
setAsChild(asChild);
|
| 425 |
+
}, [asChild, setAsChild]);
|
| 426 |
+
|
| 427 |
+
return null;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
type TooltipTriggerProps = WithAsChild<HTMLMotionProps<'div'>>;
|
| 431 |
+
|
| 432 |
+
function TooltipTrigger({
|
| 433 |
+
ref,
|
| 434 |
+
onMouseEnter,
|
| 435 |
+
onMouseLeave,
|
| 436 |
+
onFocus,
|
| 437 |
+
onBlur,
|
| 438 |
+
onPointerDown,
|
| 439 |
+
asChild = false,
|
| 440 |
+
...props
|
| 441 |
+
}: TooltipTriggerProps) {
|
| 442 |
+
const {
|
| 443 |
+
props: contentProps,
|
| 444 |
+
asChild: contentAsChild,
|
| 445 |
+
side,
|
| 446 |
+
sideOffset,
|
| 447 |
+
align,
|
| 448 |
+
alignOffset,
|
| 449 |
+
id,
|
| 450 |
+
} = useTooltip();
|
| 451 |
+
const {
|
| 452 |
+
showTooltip,
|
| 453 |
+
hideTooltip,
|
| 454 |
+
hideImmediate,
|
| 455 |
+
currentTooltip,
|
| 456 |
+
setReferenceEl,
|
| 457 |
+
} = useGlobalTooltip();
|
| 458 |
+
|
| 459 |
+
const triggerRef = React.useRef<HTMLDivElement>(null);
|
| 460 |
+
React.useImperativeHandle(ref, () => triggerRef.current as HTMLDivElement);
|
| 461 |
+
|
| 462 |
+
const suppressNextFocusRef = React.useRef(false);
|
| 463 |
+
|
| 464 |
+
const handleOpen = React.useCallback(() => {
|
| 465 |
+
if (!triggerRef.current) return;
|
| 466 |
+
setReferenceEl(triggerRef.current);
|
| 467 |
+
const rect = triggerRef.current.getBoundingClientRect();
|
| 468 |
+
showTooltip({
|
| 469 |
+
contentProps,
|
| 470 |
+
contentAsChild,
|
| 471 |
+
rect,
|
| 472 |
+
side,
|
| 473 |
+
sideOffset,
|
| 474 |
+
align,
|
| 475 |
+
alignOffset,
|
| 476 |
+
id,
|
| 477 |
+
});
|
| 478 |
+
}, [
|
| 479 |
+
showTooltip,
|
| 480 |
+
setReferenceEl,
|
| 481 |
+
contentProps,
|
| 482 |
+
contentAsChild,
|
| 483 |
+
side,
|
| 484 |
+
sideOffset,
|
| 485 |
+
align,
|
| 486 |
+
alignOffset,
|
| 487 |
+
id,
|
| 488 |
+
]);
|
| 489 |
+
|
| 490 |
+
const handlePointerDown = React.useCallback(
|
| 491 |
+
(e: React.PointerEvent<HTMLDivElement>) => {
|
| 492 |
+
onPointerDown?.(e);
|
| 493 |
+
if (currentTooltip?.id === id) {
|
| 494 |
+
suppressNextFocusRef.current = true;
|
| 495 |
+
hideImmediate();
|
| 496 |
+
Promise.resolve().then(() => {
|
| 497 |
+
suppressNextFocusRef.current = false;
|
| 498 |
+
});
|
| 499 |
+
}
|
| 500 |
+
},
|
| 501 |
+
[onPointerDown, currentTooltip?.id, id, hideImmediate],
|
| 502 |
+
);
|
| 503 |
+
|
| 504 |
+
const handleMouseEnter = React.useCallback(
|
| 505 |
+
(e: React.MouseEvent<HTMLDivElement>) => {
|
| 506 |
+
onMouseEnter?.(e);
|
| 507 |
+
handleOpen();
|
| 508 |
+
},
|
| 509 |
+
[handleOpen, onMouseEnter],
|
| 510 |
+
);
|
| 511 |
+
|
| 512 |
+
const handleMouseLeave = React.useCallback(
|
| 513 |
+
(e: React.MouseEvent<HTMLDivElement>) => {
|
| 514 |
+
onMouseLeave?.(e);
|
| 515 |
+
hideTooltip();
|
| 516 |
+
},
|
| 517 |
+
[hideTooltip, onMouseLeave],
|
| 518 |
+
);
|
| 519 |
+
|
| 520 |
+
const handleFocus = React.useCallback(
|
| 521 |
+
(e: React.FocusEvent<HTMLDivElement>) => {
|
| 522 |
+
onFocus?.(e);
|
| 523 |
+
if (suppressNextFocusRef.current) return;
|
| 524 |
+
handleOpen();
|
| 525 |
+
},
|
| 526 |
+
[handleOpen, onFocus],
|
| 527 |
+
);
|
| 528 |
+
|
| 529 |
+
const handleBlur = React.useCallback(
|
| 530 |
+
(e: React.FocusEvent<HTMLDivElement>) => {
|
| 531 |
+
onBlur?.(e);
|
| 532 |
+
hideTooltip();
|
| 533 |
+
},
|
| 534 |
+
[hideTooltip, onBlur],
|
| 535 |
+
);
|
| 536 |
+
|
| 537 |
+
const Component = asChild ? Slot : motion.div;
|
| 538 |
+
|
| 539 |
+
return (
|
| 540 |
+
<Component
|
| 541 |
+
ref={triggerRef}
|
| 542 |
+
onPointerDown={handlePointerDown}
|
| 543 |
+
onMouseEnter={handleMouseEnter}
|
| 544 |
+
onMouseLeave={handleMouseLeave}
|
| 545 |
+
onFocus={handleFocus}
|
| 546 |
+
onBlur={handleBlur}
|
| 547 |
+
data-slot="tooltip-trigger"
|
| 548 |
+
data-side={side}
|
| 549 |
+
data-align={align}
|
| 550 |
+
data-state={currentTooltip?.id === id ? 'open' : 'closed'}
|
| 551 |
+
{...props}
|
| 552 |
+
/>
|
| 553 |
+
);
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
export {
|
| 557 |
+
TooltipProvider,
|
| 558 |
+
Tooltip,
|
| 559 |
+
TooltipContent,
|
| 560 |
+
TooltipTrigger,
|
| 561 |
+
TooltipArrow,
|
| 562 |
+
useGlobalTooltip,
|
| 563 |
+
useTooltip,
|
| 564 |
+
type TooltipProviderProps,
|
| 565 |
+
type TooltipProps,
|
| 566 |
+
type TooltipContentProps,
|
| 567 |
+
type TooltipTriggerProps,
|
| 568 |
+
type TooltipArrowProps,
|
| 569 |
+
type TooltipPosition,
|
| 570 |
+
type GlobalTooltipContextType,
|
| 571 |
+
type TooltipContextType,
|
| 572 |
+
};
|
ui/components/animate-ui/primitives/effects/highlight.tsx
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import * as React from 'react';
|
| 4 |
+
import { AnimatePresence, motion, type Transition } from 'motion/react';
|
| 5 |
+
|
| 6 |
+
import { cn } from '@/lib/utils';
|
| 7 |
+
|
| 8 |
+
type HighlightMode = 'children' | 'parent';
|
| 9 |
+
|
| 10 |
+
type Bounds = {
|
| 11 |
+
top: number;
|
| 12 |
+
left: number;
|
| 13 |
+
width: number;
|
| 14 |
+
height: number;
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
const DEFAULT_BOUNDS_OFFSET: Bounds = {
|
| 18 |
+
top: 0,
|
| 19 |
+
left: 0,
|
| 20 |
+
width: 0,
|
| 21 |
+
height: 0,
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
type HighlightContextType<T extends string> = {
|
| 25 |
+
as?: keyof HTMLElementTagNameMap;
|
| 26 |
+
mode: HighlightMode;
|
| 27 |
+
activeValue: T | null;
|
| 28 |
+
setActiveValue: (value: T | null) => void;
|
| 29 |
+
setBounds: (bounds: DOMRect) => void;
|
| 30 |
+
clearBounds: () => void;
|
| 31 |
+
id: string;
|
| 32 |
+
hover: boolean;
|
| 33 |
+
click: boolean;
|
| 34 |
+
className?: string;
|
| 35 |
+
style?: React.CSSProperties;
|
| 36 |
+
activeClassName?: string;
|
| 37 |
+
setActiveClassName: (className: string) => void;
|
| 38 |
+
transition?: Transition;
|
| 39 |
+
disabled?: boolean;
|
| 40 |
+
enabled?: boolean;
|
| 41 |
+
exitDelay?: number;
|
| 42 |
+
forceUpdateBounds?: boolean;
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
const HighlightContext = React.createContext<
|
| 46 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 47 |
+
HighlightContextType<any> | undefined
|
| 48 |
+
>(undefined);
|
| 49 |
+
|
| 50 |
+
function useHighlight<T extends string>(): HighlightContextType<T> {
|
| 51 |
+
const context = React.useContext(HighlightContext);
|
| 52 |
+
if (!context) {
|
| 53 |
+
throw new Error('useHighlight must be used within a HighlightProvider');
|
| 54 |
+
}
|
| 55 |
+
return context as unknown as HighlightContextType<T>;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
type BaseHighlightProps<T extends React.ElementType = 'div'> = {
|
| 59 |
+
as?: T;
|
| 60 |
+
ref?: React.Ref<HTMLDivElement>;
|
| 61 |
+
mode?: HighlightMode;
|
| 62 |
+
value?: string | null;
|
| 63 |
+
defaultValue?: string | null;
|
| 64 |
+
onValueChange?: (value: string | null) => void;
|
| 65 |
+
className?: string;
|
| 66 |
+
style?: React.CSSProperties;
|
| 67 |
+
transition?: Transition;
|
| 68 |
+
hover?: boolean;
|
| 69 |
+
click?: boolean;
|
| 70 |
+
disabled?: boolean;
|
| 71 |
+
enabled?: boolean;
|
| 72 |
+
exitDelay?: number;
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
type ParentModeHighlightProps = {
|
| 76 |
+
boundsOffset?: Partial<Bounds>;
|
| 77 |
+
containerClassName?: string;
|
| 78 |
+
forceUpdateBounds?: boolean;
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
type ControlledParentModeHighlightProps<T extends React.ElementType = 'div'> =
|
| 82 |
+
BaseHighlightProps<T> &
|
| 83 |
+
ParentModeHighlightProps & {
|
| 84 |
+
mode: 'parent';
|
| 85 |
+
controlledItems: true;
|
| 86 |
+
children: React.ReactNode;
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
type ControlledChildrenModeHighlightProps<T extends React.ElementType = 'div'> =
|
| 90 |
+
BaseHighlightProps<T> & {
|
| 91 |
+
mode?: 'children' | undefined;
|
| 92 |
+
controlledItems: true;
|
| 93 |
+
children: React.ReactNode;
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
type UncontrolledParentModeHighlightProps<T extends React.ElementType = 'div'> =
|
| 97 |
+
BaseHighlightProps<T> &
|
| 98 |
+
ParentModeHighlightProps & {
|
| 99 |
+
mode: 'parent';
|
| 100 |
+
controlledItems?: false;
|
| 101 |
+
itemsClassName?: string;
|
| 102 |
+
children: React.ReactElement | React.ReactElement[];
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
type UncontrolledChildrenModeHighlightProps<
|
| 106 |
+
T extends React.ElementType = 'div',
|
| 107 |
+
> = BaseHighlightProps<T> & {
|
| 108 |
+
mode?: 'children';
|
| 109 |
+
controlledItems?: false;
|
| 110 |
+
itemsClassName?: string;
|
| 111 |
+
children: React.ReactElement | React.ReactElement[];
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
type HighlightProps<T extends React.ElementType = 'div'> =
|
| 115 |
+
| ControlledParentModeHighlightProps<T>
|
| 116 |
+
| ControlledChildrenModeHighlightProps<T>
|
| 117 |
+
| UncontrolledParentModeHighlightProps<T>
|
| 118 |
+
| UncontrolledChildrenModeHighlightProps<T>;
|
| 119 |
+
|
| 120 |
+
function Highlight<T extends React.ElementType = 'div'>({
|
| 121 |
+
ref,
|
| 122 |
+
...props
|
| 123 |
+
}: HighlightProps<T>) {
|
| 124 |
+
const {
|
| 125 |
+
as: Component = 'div',
|
| 126 |
+
children,
|
| 127 |
+
value,
|
| 128 |
+
defaultValue,
|
| 129 |
+
onValueChange,
|
| 130 |
+
className,
|
| 131 |
+
style,
|
| 132 |
+
transition = { type: 'spring', stiffness: 350, damping: 35 },
|
| 133 |
+
hover = false,
|
| 134 |
+
click = true,
|
| 135 |
+
enabled = true,
|
| 136 |
+
controlledItems,
|
| 137 |
+
disabled = false,
|
| 138 |
+
exitDelay = 200,
|
| 139 |
+
mode = 'children',
|
| 140 |
+
} = props;
|
| 141 |
+
|
| 142 |
+
const localRef = React.useRef<HTMLDivElement>(null);
|
| 143 |
+
React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement);
|
| 144 |
+
|
| 145 |
+
const propsBoundsOffset = (props as ParentModeHighlightProps)?.boundsOffset;
|
| 146 |
+
const boundsOffset = propsBoundsOffset ?? DEFAULT_BOUNDS_OFFSET;
|
| 147 |
+
const boundsOffsetTop = boundsOffset.top ?? 0;
|
| 148 |
+
const boundsOffsetLeft = boundsOffset.left ?? 0;
|
| 149 |
+
const boundsOffsetWidth = boundsOffset.width ?? 0;
|
| 150 |
+
const boundsOffsetHeight = boundsOffset.height ?? 0;
|
| 151 |
+
|
| 152 |
+
const boundsOffsetRef = React.useRef({
|
| 153 |
+
top: boundsOffsetTop,
|
| 154 |
+
left: boundsOffsetLeft,
|
| 155 |
+
width: boundsOffsetWidth,
|
| 156 |
+
height: boundsOffsetHeight,
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
React.useEffect(() => {
|
| 160 |
+
boundsOffsetRef.current = {
|
| 161 |
+
top: boundsOffsetTop,
|
| 162 |
+
left: boundsOffsetLeft,
|
| 163 |
+
width: boundsOffsetWidth,
|
| 164 |
+
height: boundsOffsetHeight,
|
| 165 |
+
};
|
| 166 |
+
}, [
|
| 167 |
+
boundsOffsetTop,
|
| 168 |
+
boundsOffsetLeft,
|
| 169 |
+
boundsOffsetWidth,
|
| 170 |
+
boundsOffsetHeight,
|
| 171 |
+
]);
|
| 172 |
+
|
| 173 |
+
const [activeValue, setActiveValue] = React.useState<string | null>(
|
| 174 |
+
value ?? defaultValue ?? null,
|
| 175 |
+
);
|
| 176 |
+
const [boundsState, setBoundsState] = React.useState<Bounds | null>(null);
|
| 177 |
+
const [activeClassNameState, setActiveClassNameState] =
|
| 178 |
+
React.useState<string>('');
|
| 179 |
+
|
| 180 |
+
const safeSetActiveValue = (id: string | null) => {
|
| 181 |
+
setActiveValue((prev) => {
|
| 182 |
+
if (prev !== id) {
|
| 183 |
+
onValueChange?.(id);
|
| 184 |
+
return id;
|
| 185 |
+
}
|
| 186 |
+
return prev;
|
| 187 |
+
});
|
| 188 |
+
};
|
| 189 |
+
|
| 190 |
+
const safeSetBoundsRef = React.useRef<
|
| 191 |
+
((bounds: DOMRect) => void) | undefined
|
| 192 |
+
>(undefined);
|
| 193 |
+
|
| 194 |
+
React.useEffect(() => {
|
| 195 |
+
safeSetBoundsRef.current = (bounds: DOMRect) => {
|
| 196 |
+
if (!localRef.current) return;
|
| 197 |
+
|
| 198 |
+
const containerRect = localRef.current.getBoundingClientRect();
|
| 199 |
+
const offset = boundsOffsetRef.current;
|
| 200 |
+
const newBounds: Bounds = {
|
| 201 |
+
top: bounds.top - containerRect.top + offset.top,
|
| 202 |
+
left: bounds.left - containerRect.left + offset.left,
|
| 203 |
+
width: bounds.width + offset.width,
|
| 204 |
+
height: bounds.height + offset.height,
|
| 205 |
+
};
|
| 206 |
+
|
| 207 |
+
setBoundsState((prev) => {
|
| 208 |
+
if (
|
| 209 |
+
prev &&
|
| 210 |
+
prev.top === newBounds.top &&
|
| 211 |
+
prev.left === newBounds.left &&
|
| 212 |
+
prev.width === newBounds.width &&
|
| 213 |
+
prev.height === newBounds.height
|
| 214 |
+
) {
|
| 215 |
+
return prev;
|
| 216 |
+
}
|
| 217 |
+
return newBounds;
|
| 218 |
+
});
|
| 219 |
+
};
|
| 220 |
+
});
|
| 221 |
+
|
| 222 |
+
const safeSetBounds = (bounds: DOMRect) => {
|
| 223 |
+
safeSetBoundsRef.current?.(bounds);
|
| 224 |
+
};
|
| 225 |
+
|
| 226 |
+
const clearBounds = React.useCallback(() => {
|
| 227 |
+
setBoundsState((prev) => (prev === null ? prev : null));
|
| 228 |
+
}, []);
|
| 229 |
+
|
| 230 |
+
React.useEffect(() => {
|
| 231 |
+
if (value !== undefined) setActiveValue(value);
|
| 232 |
+
else if (defaultValue !== undefined) setActiveValue(defaultValue);
|
| 233 |
+
}, [value, defaultValue]);
|
| 234 |
+
|
| 235 |
+
const id = React.useId();
|
| 236 |
+
|
| 237 |
+
React.useEffect(() => {
|
| 238 |
+
if (mode !== 'parent') return;
|
| 239 |
+
const container = localRef.current;
|
| 240 |
+
if (!container) return;
|
| 241 |
+
|
| 242 |
+
const onScroll = () => {
|
| 243 |
+
if (!activeValue) return;
|
| 244 |
+
const activeEl = container.querySelector<HTMLElement>(
|
| 245 |
+
`[data-value="${activeValue}"][data-highlight="true"]`,
|
| 246 |
+
);
|
| 247 |
+
if (activeEl)
|
| 248 |
+
safeSetBoundsRef.current?.(activeEl.getBoundingClientRect());
|
| 249 |
+
};
|
| 250 |
+
|
| 251 |
+
container.addEventListener('scroll', onScroll, { passive: true });
|
| 252 |
+
return () => container.removeEventListener('scroll', onScroll);
|
| 253 |
+
}, [mode, activeValue]);
|
| 254 |
+
|
| 255 |
+
const render = (children: React.ReactNode) => {
|
| 256 |
+
if (mode === 'parent') {
|
| 257 |
+
return (
|
| 258 |
+
<Component
|
| 259 |
+
ref={localRef}
|
| 260 |
+
data-slot="motion-highlight-container"
|
| 261 |
+
style={{ position: 'relative', zIndex: 1 }}
|
| 262 |
+
className={(props as ParentModeHighlightProps)?.containerClassName}
|
| 263 |
+
>
|
| 264 |
+
<AnimatePresence initial={false} mode="wait">
|
| 265 |
+
{boundsState && (
|
| 266 |
+
<motion.div
|
| 267 |
+
data-slot="motion-highlight"
|
| 268 |
+
animate={{
|
| 269 |
+
top: boundsState.top,
|
| 270 |
+
left: boundsState.left,
|
| 271 |
+
width: boundsState.width,
|
| 272 |
+
height: boundsState.height,
|
| 273 |
+
opacity: 1,
|
| 274 |
+
}}
|
| 275 |
+
initial={{
|
| 276 |
+
top: boundsState.top,
|
| 277 |
+
left: boundsState.left,
|
| 278 |
+
width: boundsState.width,
|
| 279 |
+
height: boundsState.height,
|
| 280 |
+
opacity: 0,
|
| 281 |
+
}}
|
| 282 |
+
exit={{
|
| 283 |
+
opacity: 0,
|
| 284 |
+
transition: {
|
| 285 |
+
...transition,
|
| 286 |
+
delay: (transition?.delay ?? 0) + (exitDelay ?? 0) / 1000,
|
| 287 |
+
},
|
| 288 |
+
}}
|
| 289 |
+
transition={transition}
|
| 290 |
+
style={{ position: 'absolute', zIndex: 0, ...style }}
|
| 291 |
+
className={cn(className, activeClassNameState)}
|
| 292 |
+
/>
|
| 293 |
+
)}
|
| 294 |
+
</AnimatePresence>
|
| 295 |
+
{children}
|
| 296 |
+
</Component>
|
| 297 |
+
);
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
return children;
|
| 301 |
+
};
|
| 302 |
+
|
| 303 |
+
return (
|
| 304 |
+
<HighlightContext.Provider
|
| 305 |
+
value={{
|
| 306 |
+
mode,
|
| 307 |
+
activeValue,
|
| 308 |
+
setActiveValue: safeSetActiveValue,
|
| 309 |
+
id,
|
| 310 |
+
hover,
|
| 311 |
+
click,
|
| 312 |
+
className,
|
| 313 |
+
style,
|
| 314 |
+
transition,
|
| 315 |
+
disabled,
|
| 316 |
+
enabled,
|
| 317 |
+
exitDelay,
|
| 318 |
+
setBounds: safeSetBounds,
|
| 319 |
+
clearBounds,
|
| 320 |
+
activeClassName: activeClassNameState,
|
| 321 |
+
setActiveClassName: setActiveClassNameState,
|
| 322 |
+
forceUpdateBounds: (props as ParentModeHighlightProps)
|
| 323 |
+
?.forceUpdateBounds,
|
| 324 |
+
}}
|
| 325 |
+
>
|
| 326 |
+
{enabled
|
| 327 |
+
? controlledItems
|
| 328 |
+
? render(children)
|
| 329 |
+
: render(
|
| 330 |
+
React.Children.map(children, (child, index) => (
|
| 331 |
+
<HighlightItem key={index} className={props?.itemsClassName}>
|
| 332 |
+
{child}
|
| 333 |
+
</HighlightItem>
|
| 334 |
+
)),
|
| 335 |
+
)
|
| 336 |
+
: children}
|
| 337 |
+
</HighlightContext.Provider>
|
| 338 |
+
);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
function getNonOverridingDataAttributes(
|
| 342 |
+
element: React.ReactElement,
|
| 343 |
+
dataAttributes: Record<string, unknown>,
|
| 344 |
+
): Record<string, unknown> {
|
| 345 |
+
return Object.keys(dataAttributes).reduce<Record<string, unknown>>(
|
| 346 |
+
(acc, key) => {
|
| 347 |
+
if ((element.props as Record<string, unknown>)[key] === undefined) {
|
| 348 |
+
acc[key] = dataAttributes[key];
|
| 349 |
+
}
|
| 350 |
+
return acc;
|
| 351 |
+
},
|
| 352 |
+
{},
|
| 353 |
+
);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
type ExtendedChildProps = React.ComponentProps<'div'> & {
|
| 357 |
+
id?: string;
|
| 358 |
+
ref?: React.Ref<HTMLElement>;
|
| 359 |
+
'data-active'?: string;
|
| 360 |
+
'data-value'?: string;
|
| 361 |
+
'data-disabled'?: boolean;
|
| 362 |
+
'data-highlight'?: boolean;
|
| 363 |
+
'data-slot'?: string;
|
| 364 |
+
};
|
| 365 |
+
|
| 366 |
+
type HighlightItemProps<T extends React.ElementType = 'div'> =
|
| 367 |
+
React.ComponentProps<T> & {
|
| 368 |
+
as?: T;
|
| 369 |
+
children: React.ReactElement;
|
| 370 |
+
id?: string;
|
| 371 |
+
value?: string;
|
| 372 |
+
className?: string;
|
| 373 |
+
style?: React.CSSProperties;
|
| 374 |
+
transition?: Transition;
|
| 375 |
+
activeClassName?: string;
|
| 376 |
+
disabled?: boolean;
|
| 377 |
+
exitDelay?: number;
|
| 378 |
+
asChild?: boolean;
|
| 379 |
+
forceUpdateBounds?: boolean;
|
| 380 |
+
};
|
| 381 |
+
|
| 382 |
+
function HighlightItem<T extends React.ElementType>({
|
| 383 |
+
ref,
|
| 384 |
+
as,
|
| 385 |
+
children,
|
| 386 |
+
id,
|
| 387 |
+
value,
|
| 388 |
+
className,
|
| 389 |
+
style,
|
| 390 |
+
transition,
|
| 391 |
+
disabled = false,
|
| 392 |
+
activeClassName,
|
| 393 |
+
exitDelay,
|
| 394 |
+
asChild = false,
|
| 395 |
+
forceUpdateBounds,
|
| 396 |
+
...props
|
| 397 |
+
}: HighlightItemProps<T>) {
|
| 398 |
+
const itemId = React.useId();
|
| 399 |
+
const {
|
| 400 |
+
activeValue,
|
| 401 |
+
setActiveValue,
|
| 402 |
+
mode,
|
| 403 |
+
setBounds,
|
| 404 |
+
clearBounds,
|
| 405 |
+
hover,
|
| 406 |
+
click,
|
| 407 |
+
enabled,
|
| 408 |
+
className: contextClassName,
|
| 409 |
+
style: contextStyle,
|
| 410 |
+
transition: contextTransition,
|
| 411 |
+
id: contextId,
|
| 412 |
+
disabled: contextDisabled,
|
| 413 |
+
exitDelay: contextExitDelay,
|
| 414 |
+
forceUpdateBounds: contextForceUpdateBounds,
|
| 415 |
+
setActiveClassName,
|
| 416 |
+
} = useHighlight();
|
| 417 |
+
|
| 418 |
+
const Component = as ?? 'div';
|
| 419 |
+
const element = children as React.ReactElement<ExtendedChildProps>;
|
| 420 |
+
const childValue =
|
| 421 |
+
id ?? value ?? element.props?.['data-value'] ?? element.props?.id ?? itemId;
|
| 422 |
+
const isActive = activeValue === childValue;
|
| 423 |
+
const isDisabled = disabled === undefined ? contextDisabled : disabled;
|
| 424 |
+
const itemTransition = transition ?? contextTransition;
|
| 425 |
+
|
| 426 |
+
const localRef = React.useRef<HTMLDivElement>(null);
|
| 427 |
+
React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement);
|
| 428 |
+
|
| 429 |
+
const refCallback = React.useCallback((node: HTMLElement | null) => {
|
| 430 |
+
localRef.current = node as HTMLDivElement;
|
| 431 |
+
}, []);
|
| 432 |
+
|
| 433 |
+
React.useEffect(() => {
|
| 434 |
+
if (mode !== 'parent') return;
|
| 435 |
+
let rafId: number;
|
| 436 |
+
let previousBounds: Bounds | null = null;
|
| 437 |
+
const shouldUpdateBounds =
|
| 438 |
+
forceUpdateBounds === true ||
|
| 439 |
+
(contextForceUpdateBounds && forceUpdateBounds !== false);
|
| 440 |
+
|
| 441 |
+
const updateBounds = () => {
|
| 442 |
+
if (!localRef.current) return;
|
| 443 |
+
|
| 444 |
+
const bounds = localRef.current.getBoundingClientRect();
|
| 445 |
+
|
| 446 |
+
if (shouldUpdateBounds) {
|
| 447 |
+
if (
|
| 448 |
+
previousBounds &&
|
| 449 |
+
previousBounds.top === bounds.top &&
|
| 450 |
+
previousBounds.left === bounds.left &&
|
| 451 |
+
previousBounds.width === bounds.width &&
|
| 452 |
+
previousBounds.height === bounds.height
|
| 453 |
+
) {
|
| 454 |
+
rafId = requestAnimationFrame(updateBounds);
|
| 455 |
+
return;
|
| 456 |
+
}
|
| 457 |
+
previousBounds = bounds;
|
| 458 |
+
rafId = requestAnimationFrame(updateBounds);
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
setBounds(bounds);
|
| 462 |
+
};
|
| 463 |
+
|
| 464 |
+
if (isActive) {
|
| 465 |
+
updateBounds();
|
| 466 |
+
setActiveClassName(activeClassName ?? '');
|
| 467 |
+
} else if (!activeValue) clearBounds();
|
| 468 |
+
|
| 469 |
+
if (shouldUpdateBounds) return () => cancelAnimationFrame(rafId);
|
| 470 |
+
}, [
|
| 471 |
+
mode,
|
| 472 |
+
isActive,
|
| 473 |
+
activeValue,
|
| 474 |
+
setBounds,
|
| 475 |
+
clearBounds,
|
| 476 |
+
activeClassName,
|
| 477 |
+
setActiveClassName,
|
| 478 |
+
forceUpdateBounds,
|
| 479 |
+
contextForceUpdateBounds,
|
| 480 |
+
]);
|
| 481 |
+
|
| 482 |
+
if (!React.isValidElement(children)) return children;
|
| 483 |
+
|
| 484 |
+
const dataAttributes = {
|
| 485 |
+
'data-active': isActive ? 'true' : 'false',
|
| 486 |
+
'aria-selected': isActive,
|
| 487 |
+
'data-disabled': isDisabled,
|
| 488 |
+
'data-value': childValue,
|
| 489 |
+
'data-highlight': true,
|
| 490 |
+
};
|
| 491 |
+
|
| 492 |
+
const commonHandlers = hover
|
| 493 |
+
? {
|
| 494 |
+
onMouseEnter: (e: React.MouseEvent<HTMLDivElement>) => {
|
| 495 |
+
setActiveValue(childValue);
|
| 496 |
+
element.props.onMouseEnter?.(e);
|
| 497 |
+
},
|
| 498 |
+
onMouseLeave: (e: React.MouseEvent<HTMLDivElement>) => {
|
| 499 |
+
setActiveValue(null);
|
| 500 |
+
element.props.onMouseLeave?.(e);
|
| 501 |
+
},
|
| 502 |
+
}
|
| 503 |
+
: click
|
| 504 |
+
? {
|
| 505 |
+
onClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
| 506 |
+
setActiveValue(childValue);
|
| 507 |
+
element.props.onClick?.(e);
|
| 508 |
+
},
|
| 509 |
+
}
|
| 510 |
+
: {};
|
| 511 |
+
|
| 512 |
+
if (asChild) {
|
| 513 |
+
if (mode === 'children') {
|
| 514 |
+
return React.cloneElement(
|
| 515 |
+
element,
|
| 516 |
+
{
|
| 517 |
+
key: childValue,
|
| 518 |
+
ref: refCallback,
|
| 519 |
+
className: cn('relative', element.props.className),
|
| 520 |
+
...getNonOverridingDataAttributes(element, {
|
| 521 |
+
...dataAttributes,
|
| 522 |
+
'data-slot': 'motion-highlight-item-container',
|
| 523 |
+
}),
|
| 524 |
+
...commonHandlers,
|
| 525 |
+
...props,
|
| 526 |
+
},
|
| 527 |
+
<>
|
| 528 |
+
<AnimatePresence initial={false} mode="wait">
|
| 529 |
+
{isActive && !isDisabled && (
|
| 530 |
+
<motion.div
|
| 531 |
+
layoutId={`transition-background-${contextId}`}
|
| 532 |
+
data-slot="motion-highlight"
|
| 533 |
+
style={{
|
| 534 |
+
position: 'absolute',
|
| 535 |
+
zIndex: 0,
|
| 536 |
+
...contextStyle,
|
| 537 |
+
...style,
|
| 538 |
+
}}
|
| 539 |
+
className={cn(contextClassName, activeClassName)}
|
| 540 |
+
transition={itemTransition}
|
| 541 |
+
initial={{ opacity: 0 }}
|
| 542 |
+
animate={{ opacity: 1 }}
|
| 543 |
+
exit={{
|
| 544 |
+
opacity: 0,
|
| 545 |
+
transition: {
|
| 546 |
+
...itemTransition,
|
| 547 |
+
delay:
|
| 548 |
+
(itemTransition?.delay ?? 0) +
|
| 549 |
+
(exitDelay ?? contextExitDelay ?? 0) / 1000,
|
| 550 |
+
},
|
| 551 |
+
}}
|
| 552 |
+
{...dataAttributes}
|
| 553 |
+
/>
|
| 554 |
+
)}
|
| 555 |
+
</AnimatePresence>
|
| 556 |
+
|
| 557 |
+
<Component
|
| 558 |
+
data-slot="motion-highlight-item"
|
| 559 |
+
style={{ position: 'relative', zIndex: 1 }}
|
| 560 |
+
className={className}
|
| 561 |
+
{...dataAttributes}
|
| 562 |
+
>
|
| 563 |
+
{children}
|
| 564 |
+
</Component>
|
| 565 |
+
</>,
|
| 566 |
+
);
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
return React.cloneElement(element, {
|
| 570 |
+
ref: refCallback,
|
| 571 |
+
...getNonOverridingDataAttributes(element, {
|
| 572 |
+
...dataAttributes,
|
| 573 |
+
'data-slot': 'motion-highlight-item',
|
| 574 |
+
}),
|
| 575 |
+
...commonHandlers,
|
| 576 |
+
});
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
return enabled ? (
|
| 580 |
+
<Component
|
| 581 |
+
key={childValue}
|
| 582 |
+
ref={localRef}
|
| 583 |
+
data-slot="motion-highlight-item-container"
|
| 584 |
+
className={cn(mode === 'children' && 'relative', className)}
|
| 585 |
+
{...dataAttributes}
|
| 586 |
+
{...props}
|
| 587 |
+
{...commonHandlers}
|
| 588 |
+
>
|
| 589 |
+
{mode === 'children' && (
|
| 590 |
+
<AnimatePresence initial={false} mode="wait">
|
| 591 |
+
{isActive && !isDisabled && (
|
| 592 |
+
<motion.div
|
| 593 |
+
layoutId={`transition-background-${contextId}`}
|
| 594 |
+
data-slot="motion-highlight"
|
| 595 |
+
style={{
|
| 596 |
+
position: 'absolute',
|
| 597 |
+
zIndex: 0,
|
| 598 |
+
...contextStyle,
|
| 599 |
+
...style,
|
| 600 |
+
}}
|
| 601 |
+
className={cn(contextClassName, activeClassName)}
|
| 602 |
+
transition={itemTransition}
|
| 603 |
+
initial={{ opacity: 0 }}
|
| 604 |
+
animate={{ opacity: 1 }}
|
| 605 |
+
exit={{
|
| 606 |
+
opacity: 0,
|
| 607 |
+
transition: {
|
| 608 |
+
...itemTransition,
|
| 609 |
+
delay:
|
| 610 |
+
(itemTransition?.delay ?? 0) +
|
| 611 |
+
(exitDelay ?? contextExitDelay ?? 0) / 1000,
|
| 612 |
+
},
|
| 613 |
+
}}
|
| 614 |
+
{...dataAttributes}
|
| 615 |
+
/>
|
| 616 |
+
)}
|
| 617 |
+
</AnimatePresence>
|
| 618 |
+
)}
|
| 619 |
+
|
| 620 |
+
{React.cloneElement(element, {
|
| 621 |
+
style: { position: 'relative', zIndex: 1 },
|
| 622 |
+
className: element.props.className,
|
| 623 |
+
...getNonOverridingDataAttributes(element, {
|
| 624 |
+
...dataAttributes,
|
| 625 |
+
'data-slot': 'motion-highlight-item',
|
| 626 |
+
}),
|
| 627 |
+
})}
|
| 628 |
+
</Component>
|
| 629 |
+
) : (
|
| 630 |
+
children
|
| 631 |
+
);
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
export {
|
| 635 |
+
Highlight,
|
| 636 |
+
HighlightItem,
|
| 637 |
+
useHighlight,
|
| 638 |
+
type HighlightProps,
|
| 639 |
+
type HighlightItemProps,
|
| 640 |
+
};
|
ui/components/animate-ui/primitives/radix/checkbox.tsx
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import * as React from 'react';
|
| 4 |
+
import { Checkbox as CheckboxPrimitive } from 'radix-ui';
|
| 5 |
+
import { motion, SVGMotionProps, type HTMLMotionProps } from 'motion/react';
|
| 6 |
+
|
| 7 |
+
import { getStrictContext } from '@/lib/get-strict-context';
|
| 8 |
+
import { useControlledState } from '@/hooks/use-controlled-state';
|
| 9 |
+
|
| 10 |
+
type CheckboxContextType = {
|
| 11 |
+
isChecked: boolean | 'indeterminate';
|
| 12 |
+
setIsChecked: (checked: boolean | 'indeterminate') => void;
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
const [CheckboxProvider, useCheckbox] =
|
| 16 |
+
getStrictContext<CheckboxContextType>('CheckboxContext');
|
| 17 |
+
|
| 18 |
+
type CheckboxProps = HTMLMotionProps<'button'> &
|
| 19 |
+
Omit<React.ComponentProps<typeof CheckboxPrimitive.Root>, 'asChild'>;
|
| 20 |
+
|
| 21 |
+
function Checkbox({
|
| 22 |
+
defaultChecked,
|
| 23 |
+
checked,
|
| 24 |
+
onCheckedChange,
|
| 25 |
+
disabled,
|
| 26 |
+
required,
|
| 27 |
+
name,
|
| 28 |
+
value,
|
| 29 |
+
...props
|
| 30 |
+
}: CheckboxProps) {
|
| 31 |
+
const [isChecked, setIsChecked] = useControlledState({
|
| 32 |
+
value: checked,
|
| 33 |
+
defaultValue: defaultChecked,
|
| 34 |
+
onChange: onCheckedChange,
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
return (
|
| 38 |
+
<CheckboxProvider value={{ isChecked, setIsChecked }}>
|
| 39 |
+
<CheckboxPrimitive.Root
|
| 40 |
+
defaultChecked={defaultChecked}
|
| 41 |
+
checked={checked}
|
| 42 |
+
onCheckedChange={setIsChecked}
|
| 43 |
+
disabled={disabled}
|
| 44 |
+
required={required}
|
| 45 |
+
name={name}
|
| 46 |
+
value={value}
|
| 47 |
+
asChild
|
| 48 |
+
>
|
| 49 |
+
<motion.button
|
| 50 |
+
data-slot="checkbox"
|
| 51 |
+
whileTap={{ scale: 0.95 }}
|
| 52 |
+
whileHover={{ scale: 1.05 }}
|
| 53 |
+
{...props}
|
| 54 |
+
/>
|
| 55 |
+
</CheckboxPrimitive.Root>
|
| 56 |
+
</CheckboxProvider>
|
| 57 |
+
);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
type CheckboxIndicatorProps = SVGMotionProps<SVGSVGElement>;
|
| 61 |
+
|
| 62 |
+
function CheckboxIndicator(props: CheckboxIndicatorProps) {
|
| 63 |
+
const { isChecked } = useCheckbox();
|
| 64 |
+
|
| 65 |
+
return (
|
| 66 |
+
<CheckboxPrimitive.Indicator forceMount asChild>
|
| 67 |
+
<motion.svg
|
| 68 |
+
data-slot="checkbox-indicator"
|
| 69 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 70 |
+
fill="none"
|
| 71 |
+
viewBox="0 0 24 24"
|
| 72 |
+
strokeWidth="3.5"
|
| 73 |
+
stroke="currentColor"
|
| 74 |
+
initial="unchecked"
|
| 75 |
+
animate={isChecked ? 'checked' : 'unchecked'}
|
| 76 |
+
{...props}
|
| 77 |
+
>
|
| 78 |
+
{isChecked === 'indeterminate' ? (
|
| 79 |
+
<motion.line
|
| 80 |
+
x1="5"
|
| 81 |
+
y1="12"
|
| 82 |
+
x2="19"
|
| 83 |
+
y2="12"
|
| 84 |
+
strokeLinecap="round"
|
| 85 |
+
initial={{ pathLength: 0, opacity: 0 }}
|
| 86 |
+
animate={{
|
| 87 |
+
pathLength: 1,
|
| 88 |
+
opacity: 1,
|
| 89 |
+
transition: { duration: 0.2 },
|
| 90 |
+
}}
|
| 91 |
+
/>
|
| 92 |
+
) : (
|
| 93 |
+
<motion.path
|
| 94 |
+
strokeLinecap="round"
|
| 95 |
+
strokeLinejoin="round"
|
| 96 |
+
d="M4.5 12.75l6 6 9-13.5"
|
| 97 |
+
variants={{
|
| 98 |
+
checked: {
|
| 99 |
+
pathLength: 1,
|
| 100 |
+
opacity: 1,
|
| 101 |
+
transition: {
|
| 102 |
+
duration: 0.2,
|
| 103 |
+
delay: 0.2,
|
| 104 |
+
},
|
| 105 |
+
},
|
| 106 |
+
unchecked: {
|
| 107 |
+
pathLength: 0,
|
| 108 |
+
opacity: 0,
|
| 109 |
+
transition: {
|
| 110 |
+
duration: 0.2,
|
| 111 |
+
},
|
| 112 |
+
},
|
| 113 |
+
}}
|
| 114 |
+
/>
|
| 115 |
+
)}
|
| 116 |
+
</motion.svg>
|
| 117 |
+
</CheckboxPrimitive.Indicator>
|
| 118 |
+
);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
export {
|
| 122 |
+
Checkbox,
|
| 123 |
+
CheckboxIndicator,
|
| 124 |
+
useCheckbox,
|
| 125 |
+
type CheckboxProps,
|
| 126 |
+
type CheckboxIndicatorProps,
|
| 127 |
+
type CheckboxContextType,
|
| 128 |
+
};
|
ui/components/animate-ui/primitives/radix/collapsible.tsx
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
| 4 |
+
|
| 5 |
+
const Collapsible = CollapsiblePrimitive.Root
|
| 6 |
+
|
| 7 |
+
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
| 8 |
+
|
| 9 |
+
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
| 10 |
+
|
| 11 |
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
ui/components/animate-ui/primitives/radix/sheet.tsx
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import * as React from 'react';
|
| 4 |
+
import { Dialog as SheetPrimitive } from 'radix-ui';
|
| 5 |
+
import { AnimatePresence, motion, type HTMLMotionProps } from 'motion/react';
|
| 6 |
+
|
| 7 |
+
import { getStrictContext } from '@/lib/get-strict-context';
|
| 8 |
+
import { useControlledState } from '@/hooks/use-controlled-state';
|
| 9 |
+
|
| 10 |
+
type SheetContextType = {
|
| 11 |
+
isOpen: boolean;
|
| 12 |
+
setIsOpen: (isOpen: boolean) => void;
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
const [SheetProvider, useSheet] =
|
| 16 |
+
getStrictContext<SheetContextType>('SheetContext');
|
| 17 |
+
|
| 18 |
+
type SheetProps = React.ComponentProps<typeof SheetPrimitive.Root>;
|
| 19 |
+
|
| 20 |
+
function Sheet(props: SheetProps) {
|
| 21 |
+
const [isOpen, setIsOpen] = useControlledState({
|
| 22 |
+
value: props.open,
|
| 23 |
+
defaultValue: props.defaultOpen,
|
| 24 |
+
onChange: props.onOpenChange,
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
return (
|
| 28 |
+
<SheetProvider value={{ isOpen, setIsOpen }}>
|
| 29 |
+
<SheetPrimitive.Root
|
| 30 |
+
data-slot="sheet"
|
| 31 |
+
{...props}
|
| 32 |
+
onOpenChange={setIsOpen}
|
| 33 |
+
/>
|
| 34 |
+
</SheetProvider>
|
| 35 |
+
);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
type SheetTriggerProps = React.ComponentProps<typeof SheetPrimitive.Trigger>;
|
| 39 |
+
|
| 40 |
+
function SheetTrigger(props: SheetTriggerProps) {
|
| 41 |
+
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
type SheetCloseProps = React.ComponentProps<typeof SheetPrimitive.Close>;
|
| 45 |
+
|
| 46 |
+
function SheetClose(props: SheetCloseProps) {
|
| 47 |
+
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
type SheetPortalProps = React.ComponentProps<typeof SheetPrimitive.Portal>;
|
| 51 |
+
|
| 52 |
+
function SheetPortal(props: SheetPortalProps) {
|
| 53 |
+
const { isOpen } = useSheet();
|
| 54 |
+
|
| 55 |
+
return (
|
| 56 |
+
<AnimatePresence>
|
| 57 |
+
{isOpen && (
|
| 58 |
+
<SheetPrimitive.Portal forceMount data-slot="sheet-portal" {...props} />
|
| 59 |
+
)}
|
| 60 |
+
</AnimatePresence>
|
| 61 |
+
);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
type SheetOverlayProps = Omit<
|
| 65 |
+
React.ComponentProps<typeof SheetPrimitive.Overlay>,
|
| 66 |
+
'asChild' | 'forceMount'
|
| 67 |
+
> &
|
| 68 |
+
HTMLMotionProps<'div'>;
|
| 69 |
+
|
| 70 |
+
function SheetOverlay({
|
| 71 |
+
transition = { duration: 0.2, ease: 'easeInOut' },
|
| 72 |
+
...props
|
| 73 |
+
}: SheetOverlayProps) {
|
| 74 |
+
return (
|
| 75 |
+
<SheetPrimitive.Overlay asChild forceMount>
|
| 76 |
+
<motion.div
|
| 77 |
+
key="sheet-overlay"
|
| 78 |
+
data-slot="sheet-overlay"
|
| 79 |
+
initial={{ opacity: 0, filter: 'blur(4px)' }}
|
| 80 |
+
animate={{ opacity: 1, filter: 'blur(0px)' }}
|
| 81 |
+
exit={{ opacity: 0, filter: 'blur(4px)' }}
|
| 82 |
+
transition={transition}
|
| 83 |
+
{...props}
|
| 84 |
+
/>
|
| 85 |
+
</SheetPrimitive.Overlay>
|
| 86 |
+
);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
type Side = 'top' | 'bottom' | 'left' | 'right';
|
| 90 |
+
|
| 91 |
+
type SheetContentProps = React.ComponentProps<typeof SheetPrimitive.Content> &
|
| 92 |
+
HTMLMotionProps<'div'> & {
|
| 93 |
+
side?: Side;
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
function SheetContent({
|
| 97 |
+
side = 'right',
|
| 98 |
+
transition = { type: 'spring', stiffness: 150, damping: 22 },
|
| 99 |
+
style,
|
| 100 |
+
children,
|
| 101 |
+
...props
|
| 102 |
+
}: SheetContentProps) {
|
| 103 |
+
const axis = side === 'left' || side === 'right' ? 'x' : 'y';
|
| 104 |
+
|
| 105 |
+
const offscreen: Record<Side, { x?: string; y?: string; opacity: number }> = {
|
| 106 |
+
right: { x: '100%', opacity: 0 },
|
| 107 |
+
left: { x: '-100%', opacity: 0 },
|
| 108 |
+
top: { y: '-100%', opacity: 0 },
|
| 109 |
+
bottom: { y: '100%', opacity: 0 },
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
const positionStyle: Record<Side, React.CSSProperties> = {
|
| 113 |
+
right: { insetBlock: 0, right: 0 },
|
| 114 |
+
left: { insetBlock: 0, left: 0 },
|
| 115 |
+
top: { insetInline: 0, top: 0 },
|
| 116 |
+
bottom: { insetInline: 0, bottom: 0 },
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
return (
|
| 120 |
+
<SheetPrimitive.Content asChild forceMount {...props}>
|
| 121 |
+
<motion.div
|
| 122 |
+
key="sheet-content"
|
| 123 |
+
data-slot="sheet-content"
|
| 124 |
+
data-side={side}
|
| 125 |
+
initial={offscreen[side]}
|
| 126 |
+
animate={{ [axis]: 0, opacity: 1 }}
|
| 127 |
+
exit={offscreen[side]}
|
| 128 |
+
style={{
|
| 129 |
+
position: 'fixed',
|
| 130 |
+
...positionStyle[side],
|
| 131 |
+
...style,
|
| 132 |
+
}}
|
| 133 |
+
transition={transition}
|
| 134 |
+
>
|
| 135 |
+
{children}
|
| 136 |
+
</motion.div>
|
| 137 |
+
</SheetPrimitive.Content>
|
| 138 |
+
);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
type SheetHeaderProps = React.ComponentProps<'div'>;
|
| 142 |
+
|
| 143 |
+
function SheetHeader(props: SheetHeaderProps) {
|
| 144 |
+
return <div data-slot="sheet-header" {...props} />;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
type SheetFooterProps = React.ComponentProps<'div'>;
|
| 148 |
+
|
| 149 |
+
function SheetFooter(props: SheetFooterProps) {
|
| 150 |
+
return <div data-slot="sheet-footer" {...props} />;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
type SheetTitleProps = React.ComponentProps<typeof SheetPrimitive.Title>;
|
| 154 |
+
|
| 155 |
+
function SheetTitle(props: SheetTitleProps) {
|
| 156 |
+
return <SheetPrimitive.Title data-slot="sheet-title" {...props} />;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
type SheetDescriptionProps = React.ComponentProps<
|
| 160 |
+
typeof SheetPrimitive.Description
|
| 161 |
+
>;
|
| 162 |
+
|
| 163 |
+
function SheetDescription(props: SheetDescriptionProps) {
|
| 164 |
+
return (
|
| 165 |
+
<SheetPrimitive.Description data-slot="sheet-description" {...props} />
|
| 166 |
+
);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
export {
|
| 170 |
+
useSheet,
|
| 171 |
+
Sheet,
|
| 172 |
+
SheetPortal,
|
| 173 |
+
SheetOverlay,
|
| 174 |
+
SheetTrigger,
|
| 175 |
+
SheetClose,
|
| 176 |
+
SheetContent,
|
| 177 |
+
SheetHeader,
|
| 178 |
+
SheetFooter,
|
| 179 |
+
SheetTitle,
|
| 180 |
+
SheetDescription,
|
| 181 |
+
type SheetProps,
|
| 182 |
+
type SheetPortalProps,
|
| 183 |
+
type SheetOverlayProps,
|
| 184 |
+
type SheetTriggerProps,
|
| 185 |
+
type SheetCloseProps,
|
| 186 |
+
type SheetContentProps,
|
| 187 |
+
type SheetHeaderProps,
|
| 188 |
+
type SheetFooterProps,
|
| 189 |
+
type SheetTitleProps,
|
| 190 |
+
type SheetDescriptionProps,
|
| 191 |
+
};
|
ui/components/page-header.tsx
CHANGED
|
@@ -1,25 +1,75 @@
|
|
| 1 |
-
// ui/components/page-header.tsx
|
| 2 |
import { cn } from "@/lib/utils"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
interface PageHeaderProps {
|
| 5 |
title: string
|
| 6 |
subtitle?: string
|
| 7 |
icon?: React.ReactNode
|
| 8 |
className?: string
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
-
export function PageHeader({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
return (
|
| 13 |
-
<div className={cn("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
<h1 className="flex items-center gap-3 text-3xl font-bold tracking-tight">
|
| 15 |
-
|
| 16 |
-
|
| 17 |
</h1>
|
| 18 |
{subtitle && (
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
)}
|
|
|
|
| 23 |
</div>
|
| 24 |
)
|
| 25 |
}
|
|
@@ -35,3 +85,5 @@ export function SectionHeader({ title, icon, action }: { title: string, icon?: R
|
|
| 35 |
</div>
|
| 36 |
)
|
| 37 |
}
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import { cn } from "@/lib/utils"
|
| 2 |
+
import { SidebarTrigger } from "@/components/animate-ui/components/radix/sidebar"
|
| 3 |
+
import { Separator } from "@/components/ui/separator"
|
| 4 |
+
import {
|
| 5 |
+
Breadcrumb,
|
| 6 |
+
BreadcrumbItem,
|
| 7 |
+
BreadcrumbLink,
|
| 8 |
+
BreadcrumbList,
|
| 9 |
+
BreadcrumbPage,
|
| 10 |
+
BreadcrumbSeparator,
|
| 11 |
+
} from "@/components/ui/breadcrumb"
|
| 12 |
|
| 13 |
interface PageHeaderProps {
|
| 14 |
title: string
|
| 15 |
subtitle?: string
|
| 16 |
icon?: React.ReactNode
|
| 17 |
className?: string
|
| 18 |
+
breadcrumbs?: Array<{ label: string; href?: string }>
|
| 19 |
+
showSidebarTrigger?: boolean
|
| 20 |
}
|
| 21 |
|
| 22 |
+
export function PageHeader({
|
| 23 |
+
title,
|
| 24 |
+
subtitle,
|
| 25 |
+
icon,
|
| 26 |
+
className,
|
| 27 |
+
breadcrumbs,
|
| 28 |
+
showSidebarTrigger = true
|
| 29 |
+
}: PageHeaderProps) {
|
| 30 |
return (
|
| 31 |
+
<div className={cn("space-y-4", className)}>
|
| 32 |
+
{(showSidebarTrigger || breadcrumbs) && (
|
| 33 |
+
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
| 34 |
+
<div className="flex items-center gap-2">
|
| 35 |
+
{showSidebarTrigger && (
|
| 36 |
+
<>
|
| 37 |
+
<SidebarTrigger className="-ml-1" />
|
| 38 |
+
<Separator orientation="vertical" className="mr-2 h-4" />
|
| 39 |
+
</>
|
| 40 |
+
)}
|
| 41 |
+
{breadcrumbs && breadcrumbs.length > 0 && (
|
| 42 |
+
<Breadcrumb>
|
| 43 |
+
<BreadcrumbList>
|
| 44 |
+
{breadcrumbs.map((crumb, index) => (
|
| 45 |
+
<React.Fragment key={index}>
|
| 46 |
+
{index > 0 && <BreadcrumbSeparator className="hidden md:block" />}
|
| 47 |
+
<BreadcrumbItem className={index === 0 ? "hidden md:block" : ""}>
|
| 48 |
+
{crumb.href ? (
|
| 49 |
+
<BreadcrumbLink href={crumb.href}>{crumb.label}</BreadcrumbLink>
|
| 50 |
+
) : (
|
| 51 |
+
<BreadcrumbPage>{crumb.label}</BreadcrumbPage>
|
| 52 |
+
)}
|
| 53 |
+
</BreadcrumbItem>
|
| 54 |
+
</React.Fragment>
|
| 55 |
+
))}
|
| 56 |
+
</BreadcrumbList>
|
| 57 |
+
</Breadcrumb>
|
| 58 |
+
)}
|
| 59 |
+
</div>
|
| 60 |
+
</header>
|
| 61 |
+
)}
|
| 62 |
+
<div className="space-y-2">
|
| 63 |
<h1 className="flex items-center gap-3 text-3xl font-bold tracking-tight">
|
| 64 |
+
{icon && <span className="text-primary">{icon}</span>}
|
| 65 |
+
{title}
|
| 66 |
</h1>
|
| 67 |
{subtitle && (
|
| 68 |
+
<p className="text-lg text-muted-foreground">
|
| 69 |
+
{subtitle}
|
| 70 |
+
</p>
|
| 71 |
)}
|
| 72 |
+
</div>
|
| 73 |
</div>
|
| 74 |
)
|
| 75 |
}
|
|
|
|
| 85 |
</div>
|
| 86 |
)
|
| 87 |
}
|
| 88 |
+
|
| 89 |
+
import * as React from "react"
|
ui/components/sidebar.tsx
CHANGED
|
@@ -1,95 +1,315 @@
|
|
| 1 |
// ui/components/sidebar.tsx
|
| 2 |
-
|
| 3 |
|
| 4 |
-
import
|
| 5 |
-
import
|
| 6 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
import {
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
const
|
| 12 |
{
|
| 13 |
-
title:
|
| 14 |
-
|
| 15 |
icon: Home,
|
|
|
|
| 16 |
},
|
| 17 |
{
|
| 18 |
-
title:
|
| 19 |
-
|
| 20 |
icon: Microscope,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
},
|
| 22 |
{
|
| 23 |
-
title:
|
| 24 |
-
|
| 25 |
icon: Dna,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
},
|
| 27 |
{
|
| 28 |
-
title:
|
| 29 |
-
|
| 30 |
icon: BarChart2,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
},
|
| 32 |
{
|
| 33 |
-
title:
|
| 34 |
-
|
| 35 |
icon: Settings,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
},
|
| 37 |
-
]
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
return (
|
| 43 |
-
<
|
| 44 |
-
<
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
-
|
| 79 |
-
<
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
| 81 |
<Terminal className="h-4 w-4 text-muted-foreground" />
|
| 82 |
<span className="text-xs font-medium">Status</span>
|
| 83 |
-
|
| 84 |
-
|
| 85 |
<span className="relative flex h-2 w-2">
|
| 86 |
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
| 87 |
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
| 88 |
</span>
|
| 89 |
<span className="text-xs text-muted-foreground">System Online</span>
|
|
|
|
| 90 |
</div>
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
}
|
|
|
|
| 1 |
// ui/components/sidebar.tsx
|
| 2 |
+
'use client';
|
| 3 |
|
| 4 |
+
import * as React from 'react';
|
| 5 |
+
import Link from 'next/link';
|
| 6 |
+
import { usePathname } from 'next/navigation';
|
| 7 |
+
import {
|
| 8 |
+
Home,
|
| 9 |
+
Microscope,
|
| 10 |
+
Dna,
|
| 11 |
+
BarChart2,
|
| 12 |
+
Settings,
|
| 13 |
+
Terminal,
|
| 14 |
+
ChevronRight,
|
| 15 |
+
ChevronsUpDown,
|
| 16 |
+
BadgeCheck,
|
| 17 |
+
Bell,
|
| 18 |
+
CreditCard,
|
| 19 |
+
LogOut,
|
| 20 |
+
Sparkles,
|
| 21 |
+
User,
|
| 22 |
+
} from 'lucide-react';
|
| 23 |
|
| 24 |
+
import {
|
| 25 |
+
Sidebar,
|
| 26 |
+
SidebarHeader,
|
| 27 |
+
SidebarContent,
|
| 28 |
+
SidebarFooter,
|
| 29 |
+
SidebarRail,
|
| 30 |
+
SidebarGroup,
|
| 31 |
+
SidebarGroupLabel,
|
| 32 |
+
SidebarMenu,
|
| 33 |
+
SidebarMenuItem,
|
| 34 |
+
SidebarMenuButton,
|
| 35 |
+
SidebarMenuSub,
|
| 36 |
+
SidebarMenuSubItem,
|
| 37 |
+
SidebarMenuSubButton,
|
| 38 |
+
} from '@/components/animate-ui/components/radix/sidebar';
|
| 39 |
+
import {
|
| 40 |
+
Collapsible,
|
| 41 |
+
CollapsibleContent,
|
| 42 |
+
CollapsibleTrigger,
|
| 43 |
+
} from '@/components/animate-ui/primitives/radix/collapsible';
|
| 44 |
+
import {
|
| 45 |
+
DropdownMenu,
|
| 46 |
+
DropdownMenuContent,
|
| 47 |
+
DropdownMenuGroup,
|
| 48 |
+
DropdownMenuItem,
|
| 49 |
+
DropdownMenuLabel,
|
| 50 |
+
DropdownMenuSeparator,
|
| 51 |
+
DropdownMenuTrigger,
|
| 52 |
+
} from '@/components/animate-ui/components/radix/dropdown-menu';
|
| 53 |
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
| 54 |
+
import { useIsMobile } from '@/hooks/use-mobile';
|
| 55 |
|
| 56 |
+
const navMain = [
|
| 57 |
{
|
| 58 |
+
title: 'Home',
|
| 59 |
+
url: '/',
|
| 60 |
icon: Home,
|
| 61 |
+
isActive: true,
|
| 62 |
},
|
| 63 |
{
|
| 64 |
+
title: 'Discovery',
|
| 65 |
+
url: '/discovery',
|
| 66 |
icon: Microscope,
|
| 67 |
+
items: [
|
| 68 |
+
{
|
| 69 |
+
title: 'Drug Discovery',
|
| 70 |
+
url: '/discovery',
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
title: 'Molecule Search',
|
| 74 |
+
url: '/discovery#search',
|
| 75 |
+
},
|
| 76 |
+
],
|
| 77 |
},
|
| 78 |
{
|
| 79 |
+
title: 'Explorer',
|
| 80 |
+
url: '/explorer',
|
| 81 |
icon: Dna,
|
| 82 |
+
items: [
|
| 83 |
+
{
|
| 84 |
+
title: 'Embeddings',
|
| 85 |
+
url: '/explorer',
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
title: 'Predictions',
|
| 89 |
+
url: '/explorer#predictions',
|
| 90 |
+
},
|
| 91 |
+
],
|
| 92 |
},
|
| 93 |
{
|
| 94 |
+
title: 'Data',
|
| 95 |
+
url: '/data',
|
| 96 |
icon: BarChart2,
|
| 97 |
+
items: [
|
| 98 |
+
{
|
| 99 |
+
title: 'Datasets',
|
| 100 |
+
url: '/data',
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
title: 'Analytics',
|
| 104 |
+
url: '/data#analytics',
|
| 105 |
+
},
|
| 106 |
+
],
|
| 107 |
},
|
| 108 |
{
|
| 109 |
+
title: 'Settings',
|
| 110 |
+
url: '/settings',
|
| 111 |
icon: Settings,
|
| 112 |
+
items: [
|
| 113 |
+
{
|
| 114 |
+
title: 'General',
|
| 115 |
+
url: '/settings',
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
title: 'Models',
|
| 119 |
+
url: '/settings#models',
|
| 120 |
+
},
|
| 121 |
+
],
|
| 122 |
},
|
| 123 |
+
];
|
| 124 |
|
| 125 |
+
const userData = {
|
| 126 |
+
name: 'BioFlow User',
|
| 127 |
+
email: 'user@bioflow.ai',
|
| 128 |
+
avatar: '',
|
| 129 |
+
};
|
| 130 |
+
|
| 131 |
+
export function AppSidebar() {
|
| 132 |
+
const isMobile = useIsMobile();
|
| 133 |
+
const pathname = usePathname();
|
| 134 |
|
| 135 |
return (
|
| 136 |
+
<Sidebar collapsible="icon">
|
| 137 |
+
<SidebarHeader>
|
| 138 |
+
{/* App Header */}
|
| 139 |
+
<SidebarMenu>
|
| 140 |
+
<SidebarMenuItem>
|
| 141 |
+
<SidebarMenuButton size="lg" asChild>
|
| 142 |
+
<Link href="/">
|
| 143 |
+
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
| 144 |
+
<Dna className="size-4" />
|
| 145 |
+
</div>
|
| 146 |
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
| 147 |
+
<span className="truncate font-semibold">BioFlow</span>
|
| 148 |
+
<span className="truncate text-xs">AI Drug Discovery</span>
|
| 149 |
+
</div>
|
| 150 |
+
</Link>
|
| 151 |
+
</SidebarMenuButton>
|
| 152 |
+
</SidebarMenuItem>
|
| 153 |
+
</SidebarMenu>
|
| 154 |
+
{/* App Header */}
|
| 155 |
+
</SidebarHeader>
|
| 156 |
+
|
| 157 |
+
<SidebarContent>
|
| 158 |
+
{/* Nav Main */}
|
| 159 |
+
<SidebarGroup>
|
| 160 |
+
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
| 161 |
+
<SidebarMenu>
|
| 162 |
+
{navMain.map((item) => {
|
| 163 |
+
const isActive = pathname === item.url || pathname?.startsWith(item.url + '/');
|
| 164 |
+
|
| 165 |
+
if (!item.items || item.items.length === 0) {
|
| 166 |
+
return (
|
| 167 |
+
<SidebarMenuItem key={item.title}>
|
| 168 |
+
<SidebarMenuButton asChild isActive={isActive} tooltip={item.title}>
|
| 169 |
+
<Link href={item.url}>
|
| 170 |
+
{item.icon && <item.icon />}
|
| 171 |
+
<span>{item.title}</span>
|
| 172 |
+
</Link>
|
| 173 |
+
</SidebarMenuButton>
|
| 174 |
+
</SidebarMenuItem>
|
| 175 |
+
);
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
return (
|
| 179 |
+
<Collapsible
|
| 180 |
+
key={item.title}
|
| 181 |
+
asChild
|
| 182 |
+
defaultOpen={isActive}
|
| 183 |
+
className="group/collapsible"
|
| 184 |
+
>
|
| 185 |
+
<SidebarMenuItem>
|
| 186 |
+
<CollapsibleTrigger asChild>
|
| 187 |
+
<SidebarMenuButton tooltip={item.title} isActive={isActive}>
|
| 188 |
+
{item.icon && <item.icon />}
|
| 189 |
+
<span>{item.title}</span>
|
| 190 |
+
<ChevronRight className="ml-auto transition-transform duration-300 group-data-[state=open]/collapsible:rotate-90" />
|
| 191 |
+
</SidebarMenuButton>
|
| 192 |
+
</CollapsibleTrigger>
|
| 193 |
+
<CollapsibleContent>
|
| 194 |
+
<SidebarMenuSub>
|
| 195 |
+
{item.items?.map((subItem) => (
|
| 196 |
+
<SidebarMenuSubItem key={subItem.title}>
|
| 197 |
+
<SidebarMenuSubButton asChild isActive={pathname === subItem.url}>
|
| 198 |
+
<Link href={subItem.url}>
|
| 199 |
+
<span>{subItem.title}</span>
|
| 200 |
+
</Link>
|
| 201 |
+
</SidebarMenuSubButton>
|
| 202 |
+
</SidebarMenuSubItem>
|
| 203 |
+
))}
|
| 204 |
+
</SidebarMenuSub>
|
| 205 |
+
</CollapsibleContent>
|
| 206 |
+
</SidebarMenuItem>
|
| 207 |
+
</Collapsible>
|
| 208 |
+
);
|
| 209 |
+
})}
|
| 210 |
+
</SidebarMenu>
|
| 211 |
+
</SidebarGroup>
|
| 212 |
+
{/* Nav Main */}
|
| 213 |
|
| 214 |
+
{/* Status Section */}
|
| 215 |
+
<SidebarGroup className="group-data-[collapsible=icon]:hidden mt-auto">
|
| 216 |
+
<SidebarGroupLabel>System Status</SidebarGroupLabel>
|
| 217 |
+
<div className="px-3 py-2">
|
| 218 |
+
<div className="rounded-lg border bg-muted/50 p-3">
|
| 219 |
+
<div className="flex items-center gap-2 mb-2">
|
| 220 |
<Terminal className="h-4 w-4 text-muted-foreground" />
|
| 221 |
<span className="text-xs font-medium">Status</span>
|
| 222 |
+
</div>
|
| 223 |
+
<div className="flex items-center gap-2">
|
| 224 |
<span className="relative flex h-2 w-2">
|
| 225 |
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
| 226 |
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
| 227 |
</span>
|
| 228 |
<span className="text-xs text-muted-foreground">System Online</span>
|
| 229 |
+
</div>
|
| 230 |
</div>
|
| 231 |
+
</div>
|
| 232 |
+
</SidebarGroup>
|
| 233 |
+
{/* Status Section */}
|
| 234 |
+
</SidebarContent>
|
| 235 |
+
|
| 236 |
+
<SidebarFooter>
|
| 237 |
+
{/* Nav User */}
|
| 238 |
+
<SidebarMenu>
|
| 239 |
+
<SidebarMenuItem>
|
| 240 |
+
<DropdownMenu>
|
| 241 |
+
<DropdownMenuTrigger asChild>
|
| 242 |
+
<SidebarMenuButton
|
| 243 |
+
size="lg"
|
| 244 |
+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
| 245 |
+
>
|
| 246 |
+
<Avatar className="h-8 w-8 rounded-lg">
|
| 247 |
+
<AvatarImage src={userData.avatar} alt={userData.name} />
|
| 248 |
+
<AvatarFallback className="rounded-lg bg-primary text-primary-foreground">
|
| 249 |
+
<User className="h-4 w-4" />
|
| 250 |
+
</AvatarFallback>
|
| 251 |
+
</Avatar>
|
| 252 |
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
| 253 |
+
<span className="truncate font-semibold">{userData.name}</span>
|
| 254 |
+
<span className="truncate text-xs">{userData.email}</span>
|
| 255 |
+
</div>
|
| 256 |
+
<ChevronsUpDown className="ml-auto size-4" />
|
| 257 |
+
</SidebarMenuButton>
|
| 258 |
+
</DropdownMenuTrigger>
|
| 259 |
+
<DropdownMenuContent
|
| 260 |
+
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
| 261 |
+
side={isMobile ? 'bottom' : 'right'}
|
| 262 |
+
align="end"
|
| 263 |
+
sideOffset={4}
|
| 264 |
+
>
|
| 265 |
+
<DropdownMenuLabel className="p-0 font-normal">
|
| 266 |
+
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
| 267 |
+
<Avatar className="h-8 w-8 rounded-lg">
|
| 268 |
+
<AvatarImage src={userData.avatar} alt={userData.name} />
|
| 269 |
+
<AvatarFallback className="rounded-lg bg-primary text-primary-foreground">
|
| 270 |
+
<User className="h-4 w-4" />
|
| 271 |
+
</AvatarFallback>
|
| 272 |
+
</Avatar>
|
| 273 |
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
| 274 |
+
<span className="truncate font-semibold">{userData.name}</span>
|
| 275 |
+
<span className="truncate text-xs">{userData.email}</span>
|
| 276 |
+
</div>
|
| 277 |
+
</div>
|
| 278 |
+
</DropdownMenuLabel>
|
| 279 |
+
<DropdownMenuSeparator />
|
| 280 |
+
<DropdownMenuGroup>
|
| 281 |
+
<DropdownMenuItem>
|
| 282 |
+
<Sparkles />
|
| 283 |
+
Upgrade to Pro
|
| 284 |
+
</DropdownMenuItem>
|
| 285 |
+
</DropdownMenuGroup>
|
| 286 |
+
<DropdownMenuSeparator />
|
| 287 |
+
<DropdownMenuGroup>
|
| 288 |
+
<DropdownMenuItem>
|
| 289 |
+
<BadgeCheck />
|
| 290 |
+
Account
|
| 291 |
+
</DropdownMenuItem>
|
| 292 |
+
<DropdownMenuItem>
|
| 293 |
+
<CreditCard />
|
| 294 |
+
Billing
|
| 295 |
+
</DropdownMenuItem>
|
| 296 |
+
<DropdownMenuItem>
|
| 297 |
+
<Bell />
|
| 298 |
+
Notifications
|
| 299 |
+
</DropdownMenuItem>
|
| 300 |
+
</DropdownMenuGroup>
|
| 301 |
+
<DropdownMenuSeparator />
|
| 302 |
+
<DropdownMenuItem>
|
| 303 |
+
<LogOut />
|
| 304 |
+
Log out
|
| 305 |
+
</DropdownMenuItem>
|
| 306 |
+
</DropdownMenuContent>
|
| 307 |
+
</DropdownMenu>
|
| 308 |
+
</SidebarMenuItem>
|
| 309 |
+
</SidebarMenu>
|
| 310 |
+
{/* Nav User */}
|
| 311 |
+
</SidebarFooter>
|
| 312 |
+
<SidebarRail />
|
| 313 |
+
</Sidebar>
|
| 314 |
+
);
|
| 315 |
}
|
ui/components/ui/avatar.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import * as React from "react"
|
| 4 |
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
| 5 |
+
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
const Avatar = React.forwardRef<
|
| 9 |
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
| 10 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
| 11 |
+
>(({ className, ...props }, ref) => (
|
| 12 |
+
<AvatarPrimitive.Root
|
| 13 |
+
ref={ref}
|
| 14 |
+
className={cn(
|
| 15 |
+
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
| 16 |
+
className
|
| 17 |
+
)}
|
| 18 |
+
{...props}
|
| 19 |
+
/>
|
| 20 |
+
))
|
| 21 |
+
Avatar.displayName = AvatarPrimitive.Root.displayName
|
| 22 |
+
|
| 23 |
+
const AvatarImage = React.forwardRef<
|
| 24 |
+
React.ElementRef<typeof AvatarPrimitive.Image>,
|
| 25 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
| 26 |
+
>(({ className, ...props }, ref) => (
|
| 27 |
+
<AvatarPrimitive.Image
|
| 28 |
+
ref={ref}
|
| 29 |
+
className={cn("aspect-square h-full w-full", className)}
|
| 30 |
+
{...props}
|
| 31 |
+
/>
|
| 32 |
+
))
|
| 33 |
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
| 34 |
+
|
| 35 |
+
const AvatarFallback = React.forwardRef<
|
| 36 |
+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
| 37 |
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
| 38 |
+
>(({ className, ...props }, ref) => (
|
| 39 |
+
<AvatarPrimitive.Fallback
|
| 40 |
+
ref={ref}
|
| 41 |
+
className={cn(
|
| 42 |
+
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
| 43 |
+
className
|
| 44 |
+
)}
|
| 45 |
+
{...props}
|
| 46 |
+
/>
|
| 47 |
+
))
|
| 48 |
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
| 49 |
+
|
| 50 |
+
export { Avatar, AvatarImage, AvatarFallback }
|
ui/components/ui/breadcrumb.tsx
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { Slot } from "@radix-ui/react-slot"
|
| 3 |
+
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { cn } from "@/lib/utils"
|
| 6 |
+
|
| 7 |
+
const Breadcrumb = React.forwardRef<
|
| 8 |
+
HTMLElement,
|
| 9 |
+
React.ComponentPropsWithoutRef<"nav"> & {
|
| 10 |
+
separator?: React.ReactNode
|
| 11 |
+
}
|
| 12 |
+
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
| 13 |
+
Breadcrumb.displayName = "Breadcrumb"
|
| 14 |
+
|
| 15 |
+
const BreadcrumbList = React.forwardRef<
|
| 16 |
+
HTMLOListElement,
|
| 17 |
+
React.ComponentPropsWithoutRef<"ol">
|
| 18 |
+
>(({ className, ...props }, ref) => (
|
| 19 |
+
<ol
|
| 20 |
+
ref={ref}
|
| 21 |
+
className={cn(
|
| 22 |
+
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
| 23 |
+
className
|
| 24 |
+
)}
|
| 25 |
+
{...props}
|
| 26 |
+
/>
|
| 27 |
+
))
|
| 28 |
+
BreadcrumbList.displayName = "BreadcrumbList"
|
| 29 |
+
|
| 30 |
+
const BreadcrumbItem = React.forwardRef<
|
| 31 |
+
HTMLLIElement,
|
| 32 |
+
React.ComponentPropsWithoutRef<"li">
|
| 33 |
+
>(({ className, ...props }, ref) => (
|
| 34 |
+
<li
|
| 35 |
+
ref={ref}
|
| 36 |
+
className={cn("inline-flex items-center gap-1.5", className)}
|
| 37 |
+
{...props}
|
| 38 |
+
/>
|
| 39 |
+
))
|
| 40 |
+
BreadcrumbItem.displayName = "BreadcrumbItem"
|
| 41 |
+
|
| 42 |
+
const BreadcrumbLink = React.forwardRef<
|
| 43 |
+
HTMLAnchorElement,
|
| 44 |
+
React.ComponentPropsWithoutRef<"a"> & {
|
| 45 |
+
asChild?: boolean
|
| 46 |
+
}
|
| 47 |
+
>(({ asChild, className, ...props }, ref) => {
|
| 48 |
+
const Comp = asChild ? Slot : "a"
|
| 49 |
+
|
| 50 |
+
return (
|
| 51 |
+
<Comp
|
| 52 |
+
ref={ref}
|
| 53 |
+
className={cn("transition-colors hover:text-foreground", className)}
|
| 54 |
+
{...props}
|
| 55 |
+
/>
|
| 56 |
+
)
|
| 57 |
+
})
|
| 58 |
+
BreadcrumbLink.displayName = "BreadcrumbLink"
|
| 59 |
+
|
| 60 |
+
const BreadcrumbPage = React.forwardRef<
|
| 61 |
+
HTMLSpanElement,
|
| 62 |
+
React.ComponentPropsWithoutRef<"span">
|
| 63 |
+
>(({ className, ...props }, ref) => (
|
| 64 |
+
<span
|
| 65 |
+
ref={ref}
|
| 66 |
+
role="link"
|
| 67 |
+
aria-disabled="true"
|
| 68 |
+
aria-current="page"
|
| 69 |
+
className={cn("font-normal text-foreground", className)}
|
| 70 |
+
{...props}
|
| 71 |
+
/>
|
| 72 |
+
))
|
| 73 |
+
BreadcrumbPage.displayName = "BreadcrumbPage"
|
| 74 |
+
|
| 75 |
+
const BreadcrumbSeparator = ({
|
| 76 |
+
children,
|
| 77 |
+
className,
|
| 78 |
+
...props
|
| 79 |
+
}: React.ComponentProps<"li">) => (
|
| 80 |
+
<li
|
| 81 |
+
role="presentation"
|
| 82 |
+
aria-hidden="true"
|
| 83 |
+
className={cn("[&>svg]:size-3.5", className)}
|
| 84 |
+
{...props}
|
| 85 |
+
>
|
| 86 |
+
{children ?? <ChevronRight />}
|
| 87 |
+
</li>
|
| 88 |
+
)
|
| 89 |
+
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
| 90 |
+
|
| 91 |
+
const BreadcrumbEllipsis = ({
|
| 92 |
+
className,
|
| 93 |
+
...props
|
| 94 |
+
}: React.ComponentProps<"span">) => (
|
| 95 |
+
<span
|
| 96 |
+
role="presentation"
|
| 97 |
+
aria-hidden="true"
|
| 98 |
+
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
| 99 |
+
{...props}
|
| 100 |
+
>
|
| 101 |
+
<MoreHorizontal className="h-4 w-4" />
|
| 102 |
+
<span className="sr-only">More</span>
|
| 103 |
+
</span>
|
| 104 |
+
)
|
| 105 |
+
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
| 106 |
+
|
| 107 |
+
export {
|
| 108 |
+
Breadcrumb,
|
| 109 |
+
BreadcrumbList,
|
| 110 |
+
BreadcrumbItem,
|
| 111 |
+
BreadcrumbLink,
|
| 112 |
+
BreadcrumbPage,
|
| 113 |
+
BreadcrumbSeparator,
|
| 114 |
+
BreadcrumbEllipsis,
|
| 115 |
+
}
|
ui/components/ui/button.tsx
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
// ui/components/ui/button.tsx
|
| 2 |
import * as React from "react"
|
| 3 |
import { Slot } from "@radix-ui/react-slot"
|
| 4 |
import { cva, type VariantProps } from "class-variance-authority"
|
|
@@ -6,26 +5,30 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|
| 6 |
import { cn } from "@/lib/utils"
|
| 7 |
|
| 8 |
const buttonVariants = cva(
|
| 9 |
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-
|
| 10 |
{
|
| 11 |
variants: {
|
| 12 |
variant: {
|
| 13 |
-
default:
|
| 14 |
-
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
| 15 |
destructive:
|
| 16 |
-
"bg-destructive text-destructive-
|
| 17 |
outline:
|
| 18 |
-
"border
|
| 19 |
secondary:
|
| 20 |
-
"bg-secondary text-secondary-foreground
|
| 21 |
-
ghost:
|
|
|
|
| 22 |
link: "text-primary underline-offset-4 hover:underline",
|
| 23 |
},
|
| 24 |
size: {
|
| 25 |
-
default: "h-9 px-4 py-2",
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
},
|
| 30 |
},
|
| 31 |
defaultVariants: {
|
|
@@ -35,24 +38,27 @@ const buttonVariants = cva(
|
|
| 35 |
}
|
| 36 |
)
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
}
|
| 55 |
-
)
|
| 56 |
-
Button.displayName = "Button"
|
| 57 |
|
| 58 |
export { Button, buttonVariants }
|
|
|
|
|
|
|
| 1 |
import * as React from "react"
|
| 2 |
import { Slot } from "@radix-ui/react-slot"
|
| 3 |
import { cva, type VariantProps } from "class-variance-authority"
|
|
|
|
| 5 |
import { cn } from "@/lib/utils"
|
| 6 |
|
| 7 |
const buttonVariants = cva(
|
| 8 |
+
"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",
|
| 9 |
{
|
| 10 |
variants: {
|
| 11 |
variant: {
|
| 12 |
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
|
|
| 13 |
destructive:
|
| 14 |
+
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
| 15 |
outline:
|
| 16 |
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
| 17 |
secondary:
|
| 18 |
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
| 19 |
+
ghost:
|
| 20 |
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
| 21 |
link: "text-primary underline-offset-4 hover:underline",
|
| 22 |
},
|
| 23 |
size: {
|
| 24 |
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
| 25 |
+
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
| 26 |
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
| 27 |
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
| 28 |
+
icon: "size-9",
|
| 29 |
+
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
| 30 |
+
"icon-sm": "size-8",
|
| 31 |
+
"icon-lg": "size-10",
|
| 32 |
},
|
| 33 |
},
|
| 34 |
defaultVariants: {
|
|
|
|
| 38 |
}
|
| 39 |
)
|
| 40 |
|
| 41 |
+
function Button({
|
| 42 |
+
className,
|
| 43 |
+
variant = "default",
|
| 44 |
+
size = "default",
|
| 45 |
+
asChild = false,
|
| 46 |
+
...props
|
| 47 |
+
}: React.ComponentProps<"button"> &
|
| 48 |
+
VariantProps<typeof buttonVariants> & {
|
| 49 |
+
asChild?: boolean
|
| 50 |
+
}) {
|
| 51 |
+
const Comp = asChild ? Slot : "button"
|
| 52 |
|
| 53 |
+
return (
|
| 54 |
+
<Comp
|
| 55 |
+
data-slot="button"
|
| 56 |
+
data-variant={variant}
|
| 57 |
+
data-size={size}
|
| 58 |
+
className={cn(buttonVariants({ variant, size, className }))}
|
| 59 |
+
{...props}
|
| 60 |
+
/>
|
| 61 |
+
)
|
| 62 |
+
}
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
export { Button, buttonVariants }
|
ui/components/ui/empty.tsx
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { cva, type VariantProps } from "class-variance-authority"
|
| 2 |
+
|
| 3 |
+
import { cn } from "@/lib/utils"
|
| 4 |
+
|
| 5 |
+
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
| 6 |
+
return (
|
| 7 |
+
<div
|
| 8 |
+
data-slot="empty"
|
| 9 |
+
className={cn(
|
| 10 |
+
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
| 11 |
+
className
|
| 12 |
+
)}
|
| 13 |
+
{...props}
|
| 14 |
+
/>
|
| 15 |
+
)
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
| 19 |
+
return (
|
| 20 |
+
<div
|
| 21 |
+
data-slot="empty-header"
|
| 22 |
+
className={cn(
|
| 23 |
+
"flex max-w-sm flex-col items-center gap-2 text-center",
|
| 24 |
+
className
|
| 25 |
+
)}
|
| 26 |
+
{...props}
|
| 27 |
+
/>
|
| 28 |
+
)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const emptyMediaVariants = cva(
|
| 32 |
+
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
| 33 |
+
{
|
| 34 |
+
variants: {
|
| 35 |
+
variant: {
|
| 36 |
+
default: "bg-transparent",
|
| 37 |
+
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
| 38 |
+
},
|
| 39 |
+
},
|
| 40 |
+
defaultVariants: {
|
| 41 |
+
variant: "default",
|
| 42 |
+
},
|
| 43 |
+
}
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
function EmptyMedia({
|
| 47 |
+
className,
|
| 48 |
+
variant = "default",
|
| 49 |
+
...props
|
| 50 |
+
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
| 51 |
+
return (
|
| 52 |
+
<div
|
| 53 |
+
data-slot="empty-icon"
|
| 54 |
+
data-variant={variant}
|
| 55 |
+
className={cn(emptyMediaVariants({ variant, className }))}
|
| 56 |
+
{...props}
|
| 57 |
+
/>
|
| 58 |
+
)
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
| 62 |
+
return (
|
| 63 |
+
<div
|
| 64 |
+
data-slot="empty-title"
|
| 65 |
+
className={cn("text-lg font-medium tracking-tight", className)}
|
| 66 |
+
{...props}
|
| 67 |
+
/>
|
| 68 |
+
)
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
| 72 |
+
return (
|
| 73 |
+
<div
|
| 74 |
+
data-slot="empty-description"
|
| 75 |
+
className={cn(
|
| 76 |
+
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
| 77 |
+
className
|
| 78 |
+
)}
|
| 79 |
+
{...props}
|
| 80 |
+
/>
|
| 81 |
+
)
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
| 85 |
+
return (
|
| 86 |
+
<div
|
| 87 |
+
data-slot="empty-content"
|
| 88 |
+
className={cn(
|
| 89 |
+
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
| 90 |
+
className
|
| 91 |
+
)}
|
| 92 |
+
{...props}
|
| 93 |
+
/>
|
| 94 |
+
)
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
export {
|
| 98 |
+
Empty,
|
| 99 |
+
EmptyHeader,
|
| 100 |
+
EmptyTitle,
|
| 101 |
+
EmptyDescription,
|
| 102 |
+
EmptyContent,
|
| 103 |
+
EmptyMedia,
|
| 104 |
+
}
|
ui/eslint.config.mjs
CHANGED
|
@@ -1,18 +1,76 @@
|
|
|
|
|
| 1 |
import { defineConfig, globalIgnores } from "eslint/config";
|
| 2 |
import nextVitals from "eslint-config-next/core-web-vitals";
|
| 3 |
import nextTs from "eslint-config-next/typescript";
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
...nextVitals,
|
| 7 |
...nextTs,
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
globalIgnores([
|
| 10 |
-
// Default ignores of eslint-config-next:
|
| 11 |
".next/**",
|
| 12 |
"out/**",
|
| 13 |
"build/**",
|
|
|
|
|
|
|
|
|
|
| 14 |
"next-env.d.ts",
|
| 15 |
]),
|
| 16 |
]);
|
| 17 |
-
|
| 18 |
-
export default eslintConfig;
|
|
|
|
| 1 |
+
// eslint.config.ts
|
| 2 |
import { defineConfig, globalIgnores } from "eslint/config";
|
| 3 |
import nextVitals from "eslint-config-next/core-web-vitals";
|
| 4 |
import nextTs from "eslint-config-next/typescript";
|
| 5 |
|
| 6 |
+
import reactHooks from "eslint-plugin-react-hooks";
|
| 7 |
+
import unusedImports from "eslint-plugin-unused-imports";
|
| 8 |
+
import simpleImportSort from "eslint-plugin-simple-import-sort";
|
| 9 |
+
import tailwindcss from "eslint-plugin-tailwindcss";
|
| 10 |
+
|
| 11 |
+
export default defineConfig([
|
| 12 |
...nextVitals,
|
| 13 |
...nextTs,
|
| 14 |
+
|
| 15 |
+
{
|
| 16 |
+
plugins: {
|
| 17 |
+
"react-hooks": reactHooks,
|
| 18 |
+
"unused-imports": unusedImports,
|
| 19 |
+
"simple-import-sort": simpleImportSort,
|
| 20 |
+
tailwindcss,
|
| 21 |
+
},
|
| 22 |
+
|
| 23 |
+
settings: {
|
| 24 |
+
// Helps eslint-plugin-tailwindcss understand cn()/cva() patterns (common with shadcn)
|
| 25 |
+
tailwindcss: {
|
| 26 |
+
callees: ["cn", "cva"],
|
| 27 |
+
// If you still have a config file, keep it here; otherwise it’s harmless.
|
| 28 |
+
config: "./tailwind.config.ts",
|
| 29 |
+
},
|
| 30 |
+
},
|
| 31 |
+
|
| 32 |
+
rules: {
|
| 33 |
+
// React hooks (Next includes some of this, but this keeps it explicit)
|
| 34 |
+
"react-hooks/rules-of-hooks": "error",
|
| 35 |
+
"react-hooks/exhaustive-deps": "warn",
|
| 36 |
+
|
| 37 |
+
// Prefer removing unused imports entirely
|
| 38 |
+
"unused-imports/no-unused-imports": "warn",
|
| 39 |
+
"unused-imports/no-unused-vars": [
|
| 40 |
+
"warn",
|
| 41 |
+
{
|
| 42 |
+
argsIgnorePattern: "^_",
|
| 43 |
+
varsIgnorePattern: "^_",
|
| 44 |
+
caughtErrorsIgnorePattern: "^_",
|
| 45 |
+
},
|
| 46 |
+
],
|
| 47 |
+
|
| 48 |
+
// If unused-imports handles it, avoid double-reporting from TS ESLint
|
| 49 |
+
"@typescript-eslint/no-unused-vars": "off",
|
| 50 |
+
|
| 51 |
+
// Practical console policy
|
| 52 |
+
"no-console": ["warn", { allow: ["info", "warn", "error"] }],
|
| 53 |
+
|
| 54 |
+
// Clean, deterministic import ordering
|
| 55 |
+
"simple-import-sort/imports": "warn",
|
| 56 |
+
"simple-import-sort/exports": "warn",
|
| 57 |
+
|
| 58 |
+
// Tailwind + shadcn friendly defaults
|
| 59 |
+
"tailwindcss/classnames-order": "warn",
|
| 60 |
+
"tailwindcss/no-contradicting-classname": "error",
|
| 61 |
+
// shadcn often uses design-token classes (e.g. bg-background, text-foreground)
|
| 62 |
+
"tailwindcss/no-custom-classname": "off",
|
| 63 |
+
},
|
| 64 |
+
},
|
| 65 |
+
|
| 66 |
+
// Ignore generated/build output (and override eslint-config-next defaults explicitly)
|
| 67 |
globalIgnores([
|
|
|
|
| 68 |
".next/**",
|
| 69 |
"out/**",
|
| 70 |
"build/**",
|
| 71 |
+
"dist/**",
|
| 72 |
+
"coverage/**",
|
| 73 |
+
"node_modules/**",
|
| 74 |
"next-env.d.ts",
|
| 75 |
]),
|
| 76 |
]);
|
|
|
|
|
|
ui/package.json
CHANGED
|
@@ -10,6 +10,10 @@
|
|
| 10 |
"type-check": "tsc --noEmit"
|
| 11 |
},
|
| 12 |
"dependencies": {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
"@radix-ui/react-label": "^2.1.8",
|
| 14 |
"@radix-ui/react-select": "^2.2.6",
|
| 15 |
"@radix-ui/react-separator": "^1.1.8",
|
|
@@ -31,12 +35,17 @@
|
|
| 31 |
"zod": "^4.3.6"
|
| 32 |
},
|
| 33 |
"devDependencies": {
|
|
|
|
| 34 |
"@tailwindcss/postcss": "^4.1.18",
|
| 35 |
"@types/node": "^25.0.10",
|
| 36 |
"@types/react": "^19.2.9",
|
| 37 |
"@types/react-dom": "^19.2.3",
|
| 38 |
"eslint": "^9.39.2",
|
| 39 |
"eslint-config-next": "^16.1.4",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
"shadcn": "^3.7.0",
|
| 41 |
"tailwindcss": "^4.1.18",
|
| 42 |
"tw-animate-css": "^1.4.0",
|
|
|
|
| 10 |
"type-check": "tsc --noEmit"
|
| 11 |
},
|
| 12 |
"dependencies": {
|
| 13 |
+
"@floating-ui/react": "^0.27.16",
|
| 14 |
+
"@radix-ui/react-avatar": "^1.1.11",
|
| 15 |
+
"@radix-ui/react-collapsible": "^1.1.12",
|
| 16 |
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
| 17 |
"@radix-ui/react-label": "^2.1.8",
|
| 18 |
"@radix-ui/react-select": "^2.2.6",
|
| 19 |
"@radix-ui/react-separator": "^1.1.8",
|
|
|
|
| 35 |
"zod": "^4.3.6"
|
| 36 |
},
|
| 37 |
"devDependencies": {
|
| 38 |
+
"@eslint/js": "^9.39.2",
|
| 39 |
"@tailwindcss/postcss": "^4.1.18",
|
| 40 |
"@types/node": "^25.0.10",
|
| 41 |
"@types/react": "^19.2.9",
|
| 42 |
"@types/react-dom": "^19.2.3",
|
| 43 |
"eslint": "^9.39.2",
|
| 44 |
"eslint-config-next": "^16.1.4",
|
| 45 |
+
"eslint-plugin-react-hooks": "^7.0.1",
|
| 46 |
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
| 47 |
+
"eslint-plugin-tailwindcss": "^3.18.2",
|
| 48 |
+
"eslint-plugin-unused-imports": "^4.3.0",
|
| 49 |
"shadcn": "^3.7.0",
|
| 50 |
"tailwindcss": "^4.1.18",
|
| 51 |
"tw-animate-css": "^1.4.0",
|
ui/pnpm-lock.yaml
CHANGED
|
@@ -8,6 +8,18 @@ importers:
|
|
| 8 |
|
| 9 |
.:
|
| 10 |
dependencies:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
'@radix-ui/react-label':
|
| 12 |
specifier: ^2.1.8
|
| 13 |
version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
|
@@ -66,6 +78,9 @@ importers:
|
|
| 66 |
specifier: ^4.3.6
|
| 67 |
version: 4.3.6
|
| 68 |
devDependencies:
|
|
|
|
|
|
|
|
|
|
| 69 |
'@tailwindcss/postcss':
|
| 70 |
specifier: ^4.1.18
|
| 71 |
version: 4.1.18
|
|
@@ -84,6 +99,18 @@ importers:
|
|
| 84 |
eslint-config-next:
|
| 85 |
specifier: ^16.1.4
|
| 86 |
version: 16.1.4(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
shadcn:
|
| 88 |
specifier: ^3.7.0
|
| 89 |
version: 3.7.0(@types/node@25.0.10)(hono@4.11.5)(typescript@5.9.3)
|
|
@@ -305,6 +332,12 @@ packages:
|
|
| 305 |
react: '>=16.8.0'
|
| 306 |
react-dom: '>=16.8.0'
|
| 307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
'@floating-ui/utils@0.2.10':
|
| 309 |
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
| 310 |
|
|
@@ -718,6 +751,19 @@ packages:
|
|
| 718 |
'@types/react-dom':
|
| 719 |
optional: true
|
| 720 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 721 |
'@radix-ui/react-checkbox@1.3.3':
|
| 722 |
resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
|
| 723 |
peerDependencies:
|
|
@@ -788,6 +834,15 @@ packages:
|
|
| 788 |
'@types/react':
|
| 789 |
optional: true
|
| 790 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 791 |
'@radix-ui/react-dialog@1.1.15':
|
| 792 |
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
|
| 793 |
peerDependencies:
|
|
@@ -2275,6 +2330,26 @@ packages:
|
|
| 2275 |
peerDependencies:
|
| 2276 |
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
|
| 2277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2278 |
eslint-scope@8.4.0:
|
| 2279 |
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
|
| 2280 |
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
|
@@ -3681,6 +3756,9 @@ packages:
|
|
| 3681 |
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
| 3682 |
engines: {node: '>= 0.4'}
|
| 3683 |
|
|
|
|
|
|
|
|
|
|
| 3684 |
tagged-tag@1.0.0:
|
| 3685 |
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
| 3686 |
engines: {node: '>=20'}
|
|
@@ -4243,6 +4321,14 @@ snapshots:
|
|
| 4243 |
react: 19.2.3
|
| 4244 |
react-dom: 19.2.3(react@19.2.3)
|
| 4245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4246 |
'@floating-ui/utils@0.2.10': {}
|
| 4247 |
|
| 4248 |
'@hono/node-server@1.19.9(hono@4.11.5)':
|
|
@@ -4584,6 +4670,19 @@ snapshots:
|
|
| 4584 |
'@types/react': 19.2.9
|
| 4585 |
'@types/react-dom': 19.2.3(@types/react@19.2.9)
|
| 4586 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4587 |
'@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
| 4588 |
dependencies:
|
| 4589 |
'@radix-ui/primitive': 1.1.3
|
|
@@ -4654,6 +4753,12 @@ snapshots:
|
|
| 4654 |
optionalDependencies:
|
| 4655 |
'@types/react': 19.2.9
|
| 4656 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4657 |
'@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
| 4658 |
dependencies:
|
| 4659 |
'@radix-ui/primitive': 1.1.3
|
|
@@ -6262,6 +6367,22 @@ snapshots:
|
|
| 6262 |
string.prototype.matchall: 4.0.12
|
| 6263 |
string.prototype.repeat: 1.0.0
|
| 6264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6265 |
eslint-scope@8.4.0:
|
| 6266 |
dependencies:
|
| 6267 |
esrecurse: 4.3.0
|
|
@@ -7830,6 +7951,8 @@ snapshots:
|
|
| 7830 |
|
| 7831 |
supports-preserve-symlinks-flag@1.0.0: {}
|
| 7832 |
|
|
|
|
|
|
|
| 7833 |
tagged-tag@1.0.0: {}
|
| 7834 |
|
| 7835 |
tailwind-merge@3.4.0: {}
|
|
|
|
| 8 |
|
| 9 |
.:
|
| 10 |
dependencies:
|
| 11 |
+
'@floating-ui/react':
|
| 12 |
+
specifier: ^0.27.16
|
| 13 |
+
version: 0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
| 14 |
+
'@radix-ui/react-avatar':
|
| 15 |
+
specifier: ^1.1.11
|
| 16 |
+
version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
| 17 |
+
'@radix-ui/react-collapsible':
|
| 18 |
+
specifier: ^1.1.12
|
| 19 |
+
version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
| 20 |
+
'@radix-ui/react-dropdown-menu':
|
| 21 |
+
specifier: ^2.1.16
|
| 22 |
+
version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
| 23 |
'@radix-ui/react-label':
|
| 24 |
specifier: ^2.1.8
|
| 25 |
version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
|
|
|
| 78 |
specifier: ^4.3.6
|
| 79 |
version: 4.3.6
|
| 80 |
devDependencies:
|
| 81 |
+
'@eslint/js':
|
| 82 |
+
specifier: ^9.39.2
|
| 83 |
+
version: 9.39.2
|
| 84 |
'@tailwindcss/postcss':
|
| 85 |
specifier: ^4.1.18
|
| 86 |
version: 4.1.18
|
|
|
|
| 99 |
eslint-config-next:
|
| 100 |
specifier: ^16.1.4
|
| 101 |
version: 16.1.4(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
| 102 |
+
eslint-plugin-react-hooks:
|
| 103 |
+
specifier: ^7.0.1
|
| 104 |
+
version: 7.0.1(eslint@9.39.2(jiti@2.6.1))
|
| 105 |
+
eslint-plugin-simple-import-sort:
|
| 106 |
+
specifier: ^12.1.1
|
| 107 |
+
version: 12.1.1(eslint@9.39.2(jiti@2.6.1))
|
| 108 |
+
eslint-plugin-tailwindcss:
|
| 109 |
+
specifier: ^3.18.2
|
| 110 |
+
version: 3.18.2(tailwindcss@4.1.18)
|
| 111 |
+
eslint-plugin-unused-imports:
|
| 112 |
+
specifier: ^4.3.0
|
| 113 |
+
version: 4.3.0(@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
|
| 114 |
shadcn:
|
| 115 |
specifier: ^3.7.0
|
| 116 |
version: 3.7.0(@types/node@25.0.10)(hono@4.11.5)(typescript@5.9.3)
|
|
|
|
| 332 |
react: '>=16.8.0'
|
| 333 |
react-dom: '>=16.8.0'
|
| 334 |
|
| 335 |
+
'@floating-ui/react@0.27.16':
|
| 336 |
+
resolution: {integrity: sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==}
|
| 337 |
+
peerDependencies:
|
| 338 |
+
react: '>=17.0.0'
|
| 339 |
+
react-dom: '>=17.0.0'
|
| 340 |
+
|
| 341 |
'@floating-ui/utils@0.2.10':
|
| 342 |
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
|
| 343 |
|
|
|
|
| 751 |
'@types/react-dom':
|
| 752 |
optional: true
|
| 753 |
|
| 754 |
+
'@radix-ui/react-avatar@1.1.11':
|
| 755 |
+
resolution: {integrity: sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==}
|
| 756 |
+
peerDependencies:
|
| 757 |
+
'@types/react': '*'
|
| 758 |
+
'@types/react-dom': '*'
|
| 759 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
| 760 |
+
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
| 761 |
+
peerDependenciesMeta:
|
| 762 |
+
'@types/react':
|
| 763 |
+
optional: true
|
| 764 |
+
'@types/react-dom':
|
| 765 |
+
optional: true
|
| 766 |
+
|
| 767 |
'@radix-ui/react-checkbox@1.3.3':
|
| 768 |
resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
|
| 769 |
peerDependencies:
|
|
|
|
| 834 |
'@types/react':
|
| 835 |
optional: true
|
| 836 |
|
| 837 |
+
'@radix-ui/react-context@1.1.3':
|
| 838 |
+
resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==}
|
| 839 |
+
peerDependencies:
|
| 840 |
+
'@types/react': '*'
|
| 841 |
+
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
| 842 |
+
peerDependenciesMeta:
|
| 843 |
+
'@types/react':
|
| 844 |
+
optional: true
|
| 845 |
+
|
| 846 |
'@radix-ui/react-dialog@1.1.15':
|
| 847 |
resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
|
| 848 |
peerDependencies:
|
|
|
|
| 2330 |
peerDependencies:
|
| 2331 |
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
|
| 2332 |
|
| 2333 |
+
eslint-plugin-simple-import-sort@12.1.1:
|
| 2334 |
+
resolution: {integrity: sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==}
|
| 2335 |
+
peerDependencies:
|
| 2336 |
+
eslint: '>=5.0.0'
|
| 2337 |
+
|
| 2338 |
+
eslint-plugin-tailwindcss@3.18.2:
|
| 2339 |
+
resolution: {integrity: sha512-QbkMLDC/OkkjFQ1iz/5jkMdHfiMu/uwujUHLAJK5iwNHD8RTxVTlsUezE0toTZ6VhybNBsk+gYGPDq2agfeRNA==}
|
| 2340 |
+
engines: {node: '>=18.12.0'}
|
| 2341 |
+
peerDependencies:
|
| 2342 |
+
tailwindcss: ^3.4.0
|
| 2343 |
+
|
| 2344 |
+
eslint-plugin-unused-imports@4.3.0:
|
| 2345 |
+
resolution: {integrity: sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA==}
|
| 2346 |
+
peerDependencies:
|
| 2347 |
+
'@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0
|
| 2348 |
+
eslint: ^9.0.0 || ^8.0.0
|
| 2349 |
+
peerDependenciesMeta:
|
| 2350 |
+
'@typescript-eslint/eslint-plugin':
|
| 2351 |
+
optional: true
|
| 2352 |
+
|
| 2353 |
eslint-scope@8.4.0:
|
| 2354 |
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
|
| 2355 |
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
|
|
|
| 3756 |
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
| 3757 |
engines: {node: '>= 0.4'}
|
| 3758 |
|
| 3759 |
+
tabbable@6.4.0:
|
| 3760 |
+
resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
|
| 3761 |
+
|
| 3762 |
tagged-tag@1.0.0:
|
| 3763 |
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
| 3764 |
engines: {node: '>=20'}
|
|
|
|
| 4321 |
react: 19.2.3
|
| 4322 |
react-dom: 19.2.3(react@19.2.3)
|
| 4323 |
|
| 4324 |
+
'@floating-ui/react@0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
| 4325 |
+
dependencies:
|
| 4326 |
+
'@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
| 4327 |
+
'@floating-ui/utils': 0.2.10
|
| 4328 |
+
react: 19.2.3
|
| 4329 |
+
react-dom: 19.2.3(react@19.2.3)
|
| 4330 |
+
tabbable: 6.4.0
|
| 4331 |
+
|
| 4332 |
'@floating-ui/utils@0.2.10': {}
|
| 4333 |
|
| 4334 |
'@hono/node-server@1.19.9(hono@4.11.5)':
|
|
|
|
| 4670 |
'@types/react': 19.2.9
|
| 4671 |
'@types/react-dom': 19.2.3(@types/react@19.2.9)
|
| 4672 |
|
| 4673 |
+
'@radix-ui/react-avatar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
| 4674 |
+
dependencies:
|
| 4675 |
+
'@radix-ui/react-context': 1.1.3(@types/react@19.2.9)(react@19.2.3)
|
| 4676 |
+
'@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
| 4677 |
+
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.9)(react@19.2.3)
|
| 4678 |
+
'@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.9)(react@19.2.3)
|
| 4679 |
+
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.9)(react@19.2.3)
|
| 4680 |
+
react: 19.2.3
|
| 4681 |
+
react-dom: 19.2.3(react@19.2.3)
|
| 4682 |
+
optionalDependencies:
|
| 4683 |
+
'@types/react': 19.2.9
|
| 4684 |
+
'@types/react-dom': 19.2.3(@types/react@19.2.9)
|
| 4685 |
+
|
| 4686 |
'@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
| 4687 |
dependencies:
|
| 4688 |
'@radix-ui/primitive': 1.1.3
|
|
|
|
| 4753 |
optionalDependencies:
|
| 4754 |
'@types/react': 19.2.9
|
| 4755 |
|
| 4756 |
+
'@radix-ui/react-context@1.1.3(@types/react@19.2.9)(react@19.2.3)':
|
| 4757 |
+
dependencies:
|
| 4758 |
+
react: 19.2.3
|
| 4759 |
+
optionalDependencies:
|
| 4760 |
+
'@types/react': 19.2.9
|
| 4761 |
+
|
| 4762 |
'@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
| 4763 |
dependencies:
|
| 4764 |
'@radix-ui/primitive': 1.1.3
|
|
|
|
| 6367 |
string.prototype.matchall: 4.0.12
|
| 6368 |
string.prototype.repeat: 1.0.0
|
| 6369 |
|
| 6370 |
+
eslint-plugin-simple-import-sort@12.1.1(eslint@9.39.2(jiti@2.6.1)):
|
| 6371 |
+
dependencies:
|
| 6372 |
+
eslint: 9.39.2(jiti@2.6.1)
|
| 6373 |
+
|
| 6374 |
+
eslint-plugin-tailwindcss@3.18.2(tailwindcss@4.1.18):
|
| 6375 |
+
dependencies:
|
| 6376 |
+
fast-glob: 3.3.3
|
| 6377 |
+
postcss: 8.5.6
|
| 6378 |
+
tailwindcss: 4.1.18
|
| 6379 |
+
|
| 6380 |
+
eslint-plugin-unused-imports@4.3.0(@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)):
|
| 6381 |
+
dependencies:
|
| 6382 |
+
eslint: 9.39.2(jiti@2.6.1)
|
| 6383 |
+
optionalDependencies:
|
| 6384 |
+
'@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
| 6385 |
+
|
| 6386 |
eslint-scope@8.4.0:
|
| 6387 |
dependencies:
|
| 6388 |
esrecurse: 4.3.0
|
|
|
|
| 7951 |
|
| 7952 |
supports-preserve-symlinks-flag@1.0.0: {}
|
| 7953 |
|
| 7954 |
+
tabbable@6.4.0: {}
|
| 7955 |
+
|
| 7956 |
tagged-tag@1.0.0: {}
|
| 7957 |
|
| 7958 |
tailwind-merge@3.4.0: {}
|