From 600320a68d9c69d4f21917d58b16360b3632df92 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Wed, 19 Mar 2025 17:52:35 +0300 Subject: [PATCH 01/10] feat: add new sidebar --- apps/dashboard/app/(app)/layout.tsx | 74 +- .../app/(app)/workspace-navigations.tsx | 11 +- apps/dashboard/components.json | 10 +- apps/dashboard/components/app-sidebar.tsx | 344 ++++++++ apps/dashboard/components/ui/breadcrumb.tsx | 115 +++ apps/dashboard/components/ui/button.tsx | 57 ++ apps/dashboard/components/ui/collapsible.tsx | 12 +- apps/dashboard/components/ui/sidebar.tsx | 773 ++++++++++++++++++ apps/dashboard/hooks/use-mobile.tsx | 19 + apps/dashboard/package.json | 2 +- internal/ui/tailwind.config.js | 1 + pnpm-lock.yaml | 8 +- 12 files changed, 1373 insertions(+), 53 deletions(-) create mode 100644 apps/dashboard/components/app-sidebar.tsx create mode 100644 apps/dashboard/components/ui/breadcrumb.tsx create mode 100644 apps/dashboard/components/ui/button.tsx create mode 100644 apps/dashboard/components/ui/sidebar.tsx create mode 100644 apps/dashboard/hooks/use-mobile.tsx diff --git a/apps/dashboard/app/(app)/layout.tsx b/apps/dashboard/app/(app)/layout.tsx index 50f8c428628..6febe18a332 100644 --- a/apps/dashboard/app/(app)/layout.tsx +++ b/apps/dashboard/app/(app)/layout.tsx @@ -1,11 +1,11 @@ +import { AppSidebar } from "@/components/app-sidebar"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { getTenantId } from "@/lib/auth"; import { db } from "@/lib/db"; import { Empty } from "@unkey/ui"; import Link from "next/link"; import { redirect } from "next/navigation"; import { UsageBanner } from "./banner"; -import { DesktopSidebar } from "./desktop-sidebar"; -import { MobileSideBar } from "./mobile-sidebar"; interface LayoutProps { children: React.ReactNode; @@ -22,47 +22,49 @@ export default async function Layout({ children }: LayoutProps) { }, }, }); + if (!workspace) { return redirect("/apis"); } return ( -
+
- - -
- - -
-
- {workspace.enabled ? ( - children - ) : ( -
- - - This workspace is disabled - - Contact{" "} - - support@unkey.dev - - - +
+ + + +
+
+ {workspace.enabled ? ( + children + ) : ( +
+ + + This workspace is disabled + + Contact{" "} + + support@unkey.dev + + + +
+ )}
- )} -
-
+
+ +
); diff --git a/apps/dashboard/app/(app)/workspace-navigations.tsx b/apps/dashboard/app/(app)/workspace-navigations.tsx index f249a0ddf0e..76c56734c33 100644 --- a/apps/dashboard/app/(app)/workspace-navigations.tsx +++ b/apps/dashboard/app/(app)/workspace-navigations.tsx @@ -14,7 +14,7 @@ import { } from "lucide-react"; import { cn } from "../../lib/utils"; -type NavItem = { +export type NavItem = { disabled?: boolean; tooltip?: string; icon: LucideIcon | React.ElementType; @@ -43,11 +43,14 @@ const DiscordIcon = () => ( ); -const Tag: React.FC<{ label: string; className?: string }> = ({ label, className }) => ( +const Tag: React.FC<{ label: string; className?: string }> = ({ + label, + className, +}) => (
{label} @@ -56,7 +59,7 @@ const Tag: React.FC<{ label: string; className?: string }> = ({ label, className export const createWorkspaceNavigation = ( workspace: Pick, - segments: string[], + segments: string[] ) => { return [ { diff --git a/apps/dashboard/components.json b/apps/dashboard/components.json index 75a2a21e3cf..fc150981a11 100644 --- a/apps/dashboard/components.json +++ b/apps/dashboard/components.json @@ -2,14 +2,20 @@ "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": true, + "tsx": true, "tailwind": { "config": "tailwind.config.js", "css": "@/styles/tailwind/tailwind.css", "baseColor": "zinc", - "cssVariables": true + "cssVariables": true, + "prefix": "" }, "aliases": { "components": "@/components", - "utils": "@/lib/utils" + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" } } + diff --git a/apps/dashboard/components/app-sidebar.tsx b/apps/dashboard/components/app-sidebar.tsx new file mode 100644 index 00000000000..857e35f93d2 --- /dev/null +++ b/apps/dashboard/components/app-sidebar.tsx @@ -0,0 +1,344 @@ +"use client"; + +import { WorkspaceSwitcher } from "@/app/(app)/team-switcher"; +import { UserButton } from "@/app/(app)/user-button"; +import { + createWorkspaceNavigation, + resourcesNavigation, + type NavItem, +} from "@/app/(app)/workspace-navigations"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarRail, +} from "@/components/ui/sidebar"; +import { useDelayLoader } from "@/hooks/useDelayLoader"; +import type { Workspace } from "@/lib/db"; +import { cn } from "@/lib/utils"; +import { ChevronRight } from "lucide-react"; +import Link from "next/link"; +import { useRouter, useSelectedLayoutSegments } from "next/navigation"; +import { useEffect, useState, useTransition } from "react"; + +// Function to create navigation items that can have sub-items +const createNestedNavigation = ( + workspace: Pick, + segments: string[] +): (NavItem & { items?: NavItem[] })[] => { + // Get the base navigation items + const baseNav = createWorkspaceNavigation(workspace, segments); + + // // For demonstration purposes, add sub-items to the APIs navigation + // // In a real implementation, you might get these from an API or config + // const nestedNav = baseNav.map((item) => { + // // Only add sub-items to the APIs navigation + // if (item.label === "APIs" && segments.at(0) === "apis") { + // return { + // ...item, + // items: [ + // { + // icon: () => null, // Sub-items typically don't need icons + // href: "/apis/license-keys", + // label: "license-keys", + // active: segments.at(1) === "license-keys", + // }, + // { + // icon: () => null, + // href: "/apis/playground", + // label: "playground", + // active: segments.at(1) === "playground", + // }, + // { + // icon: () => null, + // href: "/apis/docs-with-keys", + // label: "Docs with Keys", + // active: segments.at(1) === "docs-with-keys", + // }, + // ], + // }; + // } + // return item; + // }); + + return baseNav; +}; + +// Navigation item renderer that supports both regular and nested items +const NavItems = ({ item }: { item: NavItem & { items?: NavItem[] } }) => { + const [isPending, startTransition] = useTransition(); + const showLoader = useDelayLoader(isPending); + const router = useRouter(); + + // For loading indicators in sub-items + const [subPending, setSubPending] = useState>({}); + + const Icon = item.icon; + + // Render a flat navigation item (no sub-items) + if (!item.items || item.items.length === 0) { + return ( + + { + if (!item.external) { + startTransition(() => { + router.push(item.href); + }); + } + }} + target={item.external ? "_blank" : undefined} + > + + {showLoader ? : } + {item.label} + + + + ); + } + + // Render a collapsible navigation item with sub-items + return ( + + + + + {showLoader ? : } + {item.label} + + + + + + {item.items.map((subItem) => ( + + { + if (!subItem.external) { + // Track loading state for this specific sub-item + const updatedPending = { ...subPending }; + updatedPending[subItem.label] = true; + setSubPending(updatedPending); + + startTransition(() => { + router.push(subItem.href); + + // Reset loading state after transition + setTimeout(() => { + const resetPending = { ...subPending }; + resetPending[subItem.label] = false; + setSubPending(resetPending); + }, 300); + }); + } + }} + target={subItem.external ? "_blank" : undefined} + > + + {subPending[subItem.label] ? ( + + ) : subItem.icon ? ( + + ) : null} + {subItem.label} + + + + ))} + + + + + ); +}; + +// AppSidebar component +export function AppSidebar({ + ...props +}: React.ComponentProps & { workspace: Workspace }) { + const segments = useSelectedLayoutSegments() ?? []; + const navItems = createNestedNavigation(props.workspace, segments); + + return ( + + + + + + + WORKSPACE + + {navItems.map((item) => ( + + ))} + + + + RESOURCES + + {resourcesNavigation.map((item) => ( + + ))} + + + + + + + + + ); +} + +const AnimatedLoadingSpinner = () => { + const [segmentIndex, setSegmentIndex] = useState(0); + // Each segment ID in the order they should light up + const segments = [ + "segment-1", // Right top + "segment-2", // Right + "segment-3", // Right bottom + "segment-4", // Bottom + "segment-5", // Left bottom + "segment-6", // Left + "segment-7", // Left top + "segment-8", // Top + ]; + + useEffect(() => { + // Animate the segments in sequence + const timer = setInterval(() => { + setSegmentIndex((prevIndex) => (prevIndex + 1) % segments.length); + }, 125); // 125ms per segment = 1s for full rotation + return () => clearInterval(timer); + }, []); + + return ( + + + {segments.map((id, index) => { + // Calculate opacity based on position relative to current index + const distance = + (segments.length + index - segmentIndex) % segments.length; + const opacity = distance <= 4 ? 1 - distance * 0.2 : 0.1; + return ( + + ); + })} + + + + ); +}; + +// Helper function to get path data for each segment +function getPathForSegment(index: number) { + const paths = [ + "M13.162,3.82c-.148,0-.299-.044-.431-.136-.784-.552-1.662-.915-2.61-1.08-.407-.071-.681-.459-.61-.867,.071-.408,.459-.684,.868-.61,1.167,.203,2.248,.65,3.216,1.33,.339,.238,.42,.706,.182,1.045-.146,.208-.378,.319-.614,.319Z", + "M16.136,8.5c-.357,0-.675-.257-.738-.622-.163-.942-.527-1.82-1.082-2.608-.238-.339-.157-.807,.182-1.045,.34-.239,.809-.156,1.045,.182,.683,.97,1.132,2.052,1.334,3.214,.07,.408-.203,.796-.611,.867-.043,.008-.086,.011-.129,.011Z", + "M14.93,13.913c-.148,0-.299-.044-.431-.137-.339-.238-.42-.706-.182-1.045,.551-.784,.914-1.662,1.078-2.609,.071-.408,.466-.684,.867-.611,.408,.071,.682,.459,.611,.867-.203,1.167-.65,2.25-1.33,3.216-.146,.208-.378,.318-.614,.318Z", + "M10.249,16.887c-.357,0-.675-.257-.738-.621-.07-.408,.202-.797,.61-.868,.945-.165,1.822-.529,2.608-1.082,.34-.238,.807-.156,1.045,.182,.238,.338,.157,.807-.182,1.045-.968,.682-2.05,1.13-3.214,1.333-.044,.008-.087,.011-.13,.011Z", + "M7.751,16.885c-.043,0-.086-.003-.13-.011-1.167-.203-2.249-.651-3.216-1.33-.339-.238-.42-.706-.182-1.045,.236-.339,.702-.421,1.045-.183,.784,.551,1.662,.915,2.61,1.08,.408,.071,.681,.459,.61,.868-.063,.364-.381,.621-.738,.621Z", + "M3.072,13.911c-.236,0-.469-.111-.614-.318-.683-.97-1.132-2.052-1.334-3.214-.07-.408,.203-.796,.611-.867,.403-.073,.796,.202,.867,.61,.163,.942,.527,1.82,1.082,2.608,.238,.339,.157,.807-.182,1.045-.131,.092-.282,.137-.431,.137Z", + "M1.866,8.5c-.043,0-.086-.003-.129-.011-.408-.071-.682-.459-.611-.867,.203-1.167,.65-2.25,1.33-3.216,.236-.339,.703-.422,1.045-.182,.339,.238,.42,.706,.182,1.045-.551,.784-.914,1.662-1.078,2.609-.063,.365-.381,.622-.738,.622Z", + "M4.84,3.821c-.236,0-.468-.111-.614-.318-.238-.338-.157-.807,.182-1.045,.968-.682,2.05-1.13,3.214-1.333,.41-.072,.797,.202,.868,.61,.07,.408-.202,.797-.61,.868-.945,.165-1.822,.529-2.608,1.082-.131,.092-.282,.137-.431,.137Z", + ]; + return paths[index]; +} + +// Add CSS for the spin-slow animation +if (typeof document !== "undefined") { + const style = document.createElement("style"); + style.textContent = ` + @media (prefers-reduced-motion: reduce) { + [data-prefers-reduced-motion="respect-motion-preference"] { + animation: none !important; + transition: none !important; + } + } + + @keyframes spin-slow { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + + .animate-spin-slow { + animation: spin-slow 1.5s linear infinite; + } + `; + document.head.appendChild(style); +} diff --git a/apps/dashboard/components/ui/breadcrumb.tsx b/apps/dashboard/components/ui/breadcrumb.tsx new file mode 100644 index 00000000000..60e6c96f72f --- /dev/null +++ b/apps/dashboard/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>
- - -
- ); -}; diff --git a/apps/dashboard/app/(app)/team-switcher.tsx b/apps/dashboard/app/(app)/team-switcher.tsx index 22613825b0c..979cac1f50d 100644 --- a/apps/dashboard/app/(app)/team-switcher.tsx +++ b/apps/dashboard/app/(app)/team-switcher.tsx @@ -1,5 +1,6 @@ "use client"; import { Loading } from "@/components/dashboard/loading"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, @@ -9,22 +10,17 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { useSidebar } from "@/components/ui/sidebar"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { useOrganization, useOrganizationList, useUser } from "@clerk/nextjs"; +import { ChevronExpandY } from "@unkey/icons"; import { Check, Plus, UserPlus } from "lucide-react"; +import Link from "next/link"; import { useRouter } from "next/navigation"; import type React from "react"; import { useMemo, useState } from "react"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { useOrganization, useOrganizationList, useUser } from "@clerk/nextjs"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { ChevronExpandY } from "@unkey/icons"; -import Link from "next/link"; -import { cn } from "@/lib/utils"; -import { useSidebar } from "@/components/ui/sidebar"; type Props = { workspace: { @@ -42,11 +38,10 @@ export const WorkspaceSwitcher: React.FC = (props): JSX.Element => { const { organization: currentOrg, membership } = useOrganization(); const { user } = useUser(); const router = useRouter(); - const { isMobile, state, openMobile } = useSidebar(); + const { isMobile, state } = useSidebar(); - // When mobile sidebar is open, we want to show the full component - const isCollapsed = - (state === "collapsed" || isMobile) && !(isMobile && openMobile); + // Only collapsed in desktop mode, not in mobile mode + const isCollapsed = state === "collapsed" && !isMobile; async function changeOrg(orgId: string | null) { if (!setActive) { @@ -70,7 +65,7 @@ export const WorkspaceSwitcher: React.FC = (props): JSX.Element => { return userMemberships.data; } return userMemberships.data?.filter(({ organization }) => - organization.name.toLowerCase().includes(search.toLowerCase()) + organization.name.toLowerCase().includes(search.toLowerCase()), ); }, [search, userMemberships])!; @@ -78,24 +73,19 @@ export const WorkspaceSwitcher: React.FC = (props): JSX.Element => {
- + {currentOrg?.imageUrl ? ( - + ) : user?.imageUrl ? ( = (props): JSX.Element => { - - {props.workspace.name} - + {props.workspace.name} ) : null}
{!isCollapsed && ( - + )}
- + Personal Account = (props): JSX.Element => { > {" "} diff --git a/apps/dashboard/app/(app)/user-button.tsx b/apps/dashboard/app/(app)/user-button.tsx index 8646ee93d13..3b8a2b6b962 100644 --- a/apps/dashboard/app/(app)/user-button.tsx +++ b/apps/dashboard/app/(app)/user-button.tsx @@ -9,18 +9,14 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; +import { useSidebar } from "@/components/ui/sidebar"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; import { SignOutButton, useUser } from "@clerk/nextjs"; import { Book, ChevronRight, LogOut, Rocket, Settings } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import type React from "react"; -import { cn } from "@/lib/utils"; -import { useSidebar } from "@/components/ui/sidebar"; export const UserButton: React.FC = () => { const { user } = useUser(); @@ -28,16 +24,14 @@ export const UserButton: React.FC = () => { const { isMobile, state, openMobile } = useSidebar(); // When mobile sidebar is open, we want to show the full component - const isCollapsed = - (state === "collapsed" || isMobile) && !(isMobile && openMobile); + const isCollapsed = (state === "collapsed" || isMobile) && !(isMobile && openMobile); if (!user) { return null; } // Get user display name - const displayName = - user.username ?? user.fullName ?? user.primaryEmailAddress?.emailAddress; + const displayName = user.username ?? user.fullName ?? user.primaryEmailAddress?.emailAddress; return ( @@ -46,24 +40,21 @@ export const UserButton: React.FC = () => { "flex items-center rounded-lg hover:bg-background-subtle hover:cursor-pointer text-content", isCollapsed ? "justify-center w-10 h-10 p-0" - : "justify-between gap-2 p-2 w-auto lg:w-full h-12" + : "justify-between gap-2 p-2 w-auto lg:w-full h-12", )} >
- + {user.imageUrl ? ( - + ) : null} {(user?.fullName ?? "U").slice(0, 2).toUpperCase()} @@ -76,7 +67,7 @@ export const UserButton: React.FC = () => { "w-full overflow-hidden text-ellipsis", // On desktop: show on small/medium screens, hide on large screens // On mobile with open sidebar: always show - !isMobile && "sm:inline lg:hidden" + !isMobile && "sm:inline lg:hidden", )} asChild > @@ -99,7 +90,7 @@ export const UserButton: React.FC = () => { // Only show on large screens on desktop // On mobile with open sidebar: never show this one "hidden", - !isMobile && "lg:inline" + !isMobile && "lg:inline", )} asChild > diff --git a/apps/dashboard/app/(app)/workspace-navigations.tsx b/apps/dashboard/app/(app)/workspace-navigations.tsx index a95108eaea1..2d1289848da 100644 --- a/apps/dashboard/app/(app)/workspace-navigations.tsx +++ b/apps/dashboard/app/(app)/workspace-navigations.tsx @@ -1,17 +1,17 @@ import type { Workspace } from "@/lib/db"; -import { cn } from "../../lib/utils"; import { + BookBookmark, Fingerprint, Gauge, Gear, - BookBookmark, + Grid, InputSearch, Layers3, Nodes, ShieldKey, Sparkle3, - Grid, } from "@unkey/icons"; +import { cn } from "../../lib/utils"; export type NavItem = { disabled?: boolean; @@ -42,14 +42,11 @@ const DiscordIcon = () => ( ); -const Tag: React.FC<{ label: string; className?: string }> = ({ - label, - className, -}) => ( +const Tag: React.FC<{ label: string; className?: string }> = ({ label, className }) => (
{label} @@ -58,7 +55,7 @@ const Tag: React.FC<{ label: string; className?: string }> = ({ export const createWorkspaceNavigation = ( workspace: Pick, - segments: string[] + segments: string[], ) => { return [ { diff --git a/apps/dashboard/components.json b/apps/dashboard/components.json index fc150981a11..aad11a8bbc5 100644 --- a/apps/dashboard/components.json +++ b/apps/dashboard/components.json @@ -18,4 +18,3 @@ "hooks": "@/hooks" } } - diff --git a/apps/dashboard/components/app-sidebar.tsx b/apps/dashboard/components/app-sidebar.tsx index 0fb94cbe743..1c1484e80b2 100644 --- a/apps/dashboard/components/app-sidebar.tsx +++ b/apps/dashboard/components/app-sidebar.tsx @@ -2,21 +2,16 @@ import { WorkspaceSwitcher } from "@/app/(app)/team-switcher"; import { UserButton } from "@/app/(app)/user-button"; import { + type NavItem, createWorkspaceNavigation, resourcesNavigation, - type NavItem, } from "@/app/(app)/workspace-navigations"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, - SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, @@ -24,14 +19,13 @@ import { SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, - SidebarRail, - SidebarTrigger, useSidebar, } from "@/components/ui/sidebar"; import { useDelayLoader } from "@/hooks/useDelayLoader"; import type { Workspace } from "@/lib/db"; import { cn } from "@/lib/utils"; -import { ChevronRight, Menu } from "lucide-react"; +import { SidebarLeftHide, SidebarLeftShow } from "@unkey/icons"; +import { ChevronRight } from "lucide-react"; import Link from "next/link"; import { useRouter, useSelectedLayoutSegments } from "next/navigation"; import { useEffect, useState, useTransition } from "react"; @@ -42,14 +36,14 @@ const getButtonStyles = (isActive?: boolean, showLoader?: boolean) => { "rounded-lg transition-colors focus-visible:ring-1 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 disabled:cursor-not-allowed outline-none", "focus:border-grayA-12 focus:ring-gray-6 focus-visible:outline-none focus:ring-offset-0 drop-shadow-button", isActive ? "bg-grayA-3 text-accent-12" : "[&_svg]:text-gray-9", - showLoader ? "bg-grayA-3 [&_svg]:text-accent-12" : "" + showLoader ? "bg-grayA-3 [&_svg]:text-accent-12" : "", ); }; // Function to create navigation items that can have sub-items const createNestedNavigation = ( workspace: Pick, - segments: string[] + segments: string[], ): (NavItem & { items?: NavItem[] })[] => { // Get the base navigation items const baseNav = createWorkspaceNavigation(workspace, segments); @@ -82,7 +76,7 @@ const NavItems = ({ item }: { item: NavItem & { items?: NavItem[] } }) => { target={item.external ? "_blank" : undefined} > @@ -96,11 +90,7 @@ const NavItems = ({ item }: { item: NavItem & { items?: NavItem[] } }) => { // Render a collapsible navigation item with sub-items return ( - + { > {subPending[subItem.label] ? ( @@ -163,23 +150,6 @@ const NavItems = ({ item }: { item: NavItem & { items?: NavItem[] } }) => { ); }; -// Mobile Sidebar Trigger Component -const MobileSidebarTrigger = () => { - const { isMobile } = useSidebar(); - - if (!isMobile) { - return null; - } - - return ( -
- - - -
- ); -}; - // AppSidebar component export function AppSidebar({ ...props @@ -187,41 +157,56 @@ export function AppSidebar({ const segments = useSelectedLayoutSegments() ?? []; const navItems = createNestedNavigation(props.workspace, segments); + const { toggleSidebar, state, isMobile } = useSidebar(); + const isCollapsed = state === "collapsed"; + return ( - <> - - - + + +
- - - - - WORKSPACE - - - {navItems.map((item) => ( - - ))} - - - - - RESOURCES - - - {resourcesNavigation.map((item) => ( - - ))} - - - - - - - - - + {!isMobile && ( + <> + {!isCollapsed && ( + // biome-ignore lint/a11y/useKeyWithClickEvents: +
+ +
+ )} + {isCollapsed && ( + // biome-ignore lint/a11y/useKeyWithClickEvents: +
+ +
+ )} + + )} +
+
+ + + + {navItems.map((item) => ( + + ))} + {resourcesNavigation.map((item) => ( + + ))} + + + + + + +
); } @@ -259,8 +244,7 @@ const AnimatedLoadingSpinner = () => { {segments.map((id, index) => { // Calculate opacity based on position relative to current index - const distance = - (segments.length + index - segmentIndex) % segments.length; + const distance = (segments.length + index - segmentIndex) % segments.length; const opacity = distance <= 4 ? 1 - distance * 0.2 : 0.1; return ( { + const { isMobile, setOpenMobile } = useSidebar(); + + if (!isMobile) { + return null; + } + + return ( +
+ + +
+ + {/* TODO: Will be used in the next iteration as an indicator for */} + {/* */} +
+
+ ); +}; diff --git a/apps/dashboard/components/ui/breadcrumb.tsx b/apps/dashboard/components/ui/breadcrumb.tsx index 60e6c96f72f..d8fb62b4ceb 100644 --- a/apps/dashboard/components/ui/breadcrumb.tsx +++ b/apps/dashboard/components/ui/breadcrumb.tsx @@ -1,51 +1,45 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const Breadcrumb = React.forwardRef< HTMLElement, React.ComponentPropsWithoutRef<"nav"> & { - separator?: React.ReactNode + separator?: React.ReactNode; } ->(({ ...props }, ref) =>