diff --git a/apps/client/app/app.css b/apps/client/app/app.css index 36db4ca..42e7b74 100644 --- a/apps/client/app/app.css +++ b/apps/client/app/app.css @@ -15,6 +15,7 @@ body { overflow-x: hidden; width: 100%; position: relative; + overscroll-behavior: none; @media (prefers-color-scheme: dark) { color-scheme: dark; diff --git a/apps/client/app/components/app-sidebar.tsx b/apps/client/app/components/app-sidebar.tsx new file mode 100644 index 0000000..ca24d88 --- /dev/null +++ b/apps/client/app/components/app-sidebar.tsx @@ -0,0 +1,68 @@ +import { Database, HardDrive } from "lucide-react"; +import { NavLink } from "react-router"; +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "~/components/ui/sidebar"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"; +import { cn } from "~/lib/utils"; + +const items = [ + { + title: "Volumes", + url: "/", + icon: HardDrive, + }, + { + title: "Repositories", + url: "/repositories", + icon: Database, + }, +]; + +export function AppSidebar() { + const { state } = useSidebar(); + + return ( + + + + + + + {items.map((item) => ( + + + + + + + {({ isActive }) => ( + <> + + {item.title} + > + )} + + + + + {item.title} + + + + + ))} + + + + + + ); +} diff --git a/apps/client/app/components/layout.tsx b/apps/client/app/components/layout.tsx index 716f09f..07f0f37 100644 --- a/apps/client/app/components/layout.tsx +++ b/apps/client/app/components/layout.tsx @@ -9,6 +9,8 @@ import type { Route } from "./+types/layout"; import { AppBreadcrumb } from "./app-breadcrumb"; import { GridBackground } from "./grid-background"; import { Button } from "./ui/button"; +import { SidebarProvider, SidebarTrigger } from "./ui/sidebar"; +import { AppSidebar } from "./app-sidebar"; export const clientMiddleware = [authMiddleware]; @@ -32,38 +34,47 @@ export default function Layout({ loaderData }: Route.ComponentProps) { }); return ( - - - - - {loaderData.user && ( + + + + + - - Welcome, {loaderData.user?.username} - - logout.mutate({})} loading={logout.isPending}> - Logout - - - - - - Report an issue - - - + + - )} - - - - - - + {loaderData.user && ( + + + Welcome, + + {loaderData.user?.username[0].toUpperCase() + loaderData.user?.username.slice(1)} + + + logout.mutate({})} loading={logout.isPending}> + Logout + + + + + + Report an issue + + + + + )} + + + + + + + ); } diff --git a/apps/client/app/components/ui/separator.tsx b/apps/client/app/components/ui/separator.tsx new file mode 100644 index 0000000..8c9f9ed --- /dev/null +++ b/apps/client/app/components/ui/separator.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "~/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/apps/client/app/components/ui/sheet.tsx b/apps/client/app/components/ui/sheet.tsx new file mode 100644 index 0000000..c016b5c --- /dev/null +++ b/apps/client/app/components/ui/sheet.tsx @@ -0,0 +1,139 @@ +"use client" + +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "~/lib/utils" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" +}) { + return ( + + + + {children} + + + Close + + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/apps/client/app/components/ui/sidebar.tsx b/apps/client/app/components/ui/sidebar.tsx new file mode 100644 index 0000000..057f046 --- /dev/null +++ b/apps/client/app/components/ui/sidebar.tsx @@ -0,0 +1,677 @@ +"use client"; + +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { PanelLeftIcon } from "lucide-react"; + +import { useIsMobile } from "~/hooks/use-mobile"; +import { cn } from "~/lib/utils"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Separator } from "~/components/ui/separator"; +import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/components/ui/sheet"; +import { Skeleton } from "~/components/ui/skeleton"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"; + +const SIDEBAR_COOKIE_NAME = "sidebar_state"; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = "14rem"; +const SIDEBAR_WIDTH_MOBILE = "18rem"; +const SIDEBAR_WIDTH_ICON = "3rem"; +const SIDEBAR_KEYBOARD_SHORTCUT = "b"; + +type SidebarContextProps = { + state: "expanded" | "collapsed"; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; +}; + +const SidebarContext = React.createContext(null); + +function useSidebar() { + const context = React.useContext(SidebarContext); + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider."); + } + + return context; +} + +function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: React.ComponentProps<"div"> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; +}) { + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = React.useState(false); + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value; + if (setOpenProp) { + setOpenProp(openState); + } else { + _setOpen(openState); + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + }, + [setOpenProp, open], + ); + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); + }, [isMobile, setOpen, setOpenMobile]); + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + toggleSidebar(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [toggleSidebar]); + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed"; + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], + ); + + return ( + + + + {children} + + + + ); +} + +function Sidebar({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props +}: React.ComponentProps<"div"> & { + side?: "left" | "right"; + variant?: "sidebar" | "floating" | "inset"; + collapsible?: "offcanvas" | "icon" | "none"; +}) { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + + if (collapsible === "none") { + return ( + + {children} + + ); + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + + {children} + + + ); + } + + return ( + + {/* This is what handles the sidebar gap on desktop */} + + + + {children} + + + + ); +} + +function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps) { + const { toggleSidebar } = useSidebar(); + + return ( + { + onClick?.(event); + toggleSidebar(); + }} + {...props} + > + + Toggle Sidebar + + ); +} + +function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar(); + + return ( + + ); +} + +function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { + return ( + + ); +} + +function SidebarInput({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ); +} + +function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ); +} + +function SidebarSeparator({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ); +} + +function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ); +} + +function SidebarGroupLabel({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { asChild?: boolean }) { + const Comp = asChild ? Slot : "div"; + + return ( + svg]:size-4 [&>svg]:shrink-0", + "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", + className, + )} + {...props} + /> + ); +} + +function SidebarGroupAction({ + className, + asChild = false, + ...props +}: React.ComponentProps<"button"> & { asChild?: boolean }) { + const Comp = asChild ? Slot : "button"; + + return ( + svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 md:after:hidden", + "group-data-[collapsible=icon]:hidden", + className, + )} + {...props} + /> + ); +} + +function SidebarGroupContent({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ); +} + +function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { + return ( + + ); +} + +function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { + return ( + + ); +} + +const sidebarMenuButtonVariants = cva( + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + { + variants: { + variant: { + default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", + outline: + "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", + }, + size: { + default: "h-8 text-sm", + sm: "h-7 text-xs", + lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +function SidebarMenuButton({ + asChild = false, + isActive = false, + variant = "default", + size = "default", + tooltip, + className, + ...props +}: React.ComponentProps<"button"> & { + asChild?: boolean; + isActive?: boolean; + tooltip?: string | React.ComponentProps; +} & VariantProps) { + const Comp = asChild ? Slot : "button"; + const { isMobile, state } = useSidebar(); + + const button = ( + + ); + + if (!tooltip) { + return button; + } + + if (typeof tooltip === "string") { + tooltip = { + children: tooltip, + }; + } + + return ( + + {button} + + + ); +} + +function SidebarMenuAction({ + className, + asChild = false, + showOnHover = false, + ...props +}: React.ComponentProps<"button"> & { + asChild?: boolean; + showOnHover?: boolean; +}) { + const Comp = asChild ? Slot : "button"; + + return ( + svg]:size-4 [&>svg]:shrink-0", + // Increases the hit area of the button on mobile. + "after:absolute after:-inset-2 md:after:hidden", + "peer-data-[size=sm]/menu-button:top-1", + "peer-data-[size=default]/menu-button:top-1.5", + "peer-data-[size=lg]/menu-button:top-2.5", + "group-data-[collapsible=icon]:hidden", + showOnHover && + "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", + className, + )} + {...props} + /> + ); +} + +function SidebarMenuBadge({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ); +} + +function SidebarMenuSkeleton({ + className, + showIcon = false, + ...props +}: React.ComponentProps<"div"> & { + showIcon?: boolean; +}) { + // Random width between 50 to 90%. + const width = React.useMemo(() => { + return `${Math.floor(Math.random() * 40) + 50}%`; + }, []); + + return ( + + {showIcon && } + + + ); +} + +function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { + return ( + + ); +} + +function SidebarMenuSubItem({ className, ...props }: React.ComponentProps<"li">) { + return ( + + ); +} + +function SidebarMenuSubButton({ + asChild = false, + size = "md", + isActive = false, + className, + ...props +}: React.ComponentProps<"a"> & { + asChild?: boolean; + size?: "sm" | "md"; + isActive?: boolean; +}) { + const Comp = asChild ? Slot : "a"; + + return ( + svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground", + size === "sm" && "text-xs", + size === "md" && "text-sm", + "group-data-[collapsible=icon]:hidden", + className, + )} + {...props} + /> + ); +} + +export { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupAction, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarInput, + SidebarInset, + SidebarMenu, + SidebarMenuAction, + SidebarMenuBadge, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSkeleton, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarProvider, + SidebarRail, + SidebarSeparator, + SidebarTrigger, + useSidebar, +}; diff --git a/apps/client/app/components/ui/skeleton.tsx b/apps/client/app/components/ui/skeleton.tsx new file mode 100644 index 0000000..1e29d7a --- /dev/null +++ b/apps/client/app/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "~/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +export { Skeleton } diff --git a/apps/client/app/hooks/use-mobile.ts b/apps/client/app/hooks/use-mobile.ts new file mode 100644 index 0000000..2b0fe1d --- /dev/null +++ b/apps/client/app/hooks/use-mobile.ts @@ -0,0 +1,19 @@ +import * as React from "react" + +const MOBILE_BREAKPOINT = 768 + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState(undefined) + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + } + mql.addEventListener("change", onChange) + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + return () => mql.removeEventListener("change", onChange) + }, []) + + return !!isMobile +} diff --git a/apps/client/package.json b/apps/client/package.json index 0dc436a..120539e 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -16,6 +16,7 @@ "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", @@ -30,7 +31,7 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "isbot": "^5.1.31", - "lucide-react": "^0.544.0", + "lucide-react": "^0.546.0", "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/bun.lock b/bun.lock index 39f41ac..832dc41 100644 --- a/bun.lock +++ b/bun.lock @@ -18,6 +18,7 @@ "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", @@ -32,7 +33,7 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "isbot": "^5.1.31", - "lucide-react": "^0.544.0", + "lucide-react": "^0.546.0", "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -370,6 +371,8 @@ "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], @@ -924,7 +927,7 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@0.544.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw=="], + "lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="], "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
{item.title}