From 40e0bb0ca28b6c1e74fa862ddbbbba7d7edc7f96 Mon Sep 17 00:00:00 2001 From: dest Date: Fri, 17 Apr 2026 21:40:50 +0300 Subject: [PATCH 1/5] feat(mobile): add mobile bottom navigation and drawers --- apps/native/eslint.config.mjs | 9 +- apps/native/src/app/[locale]/layout.tsx | 6 +- apps/web/app/[locale]/layout.tsx | 1 + .../src/components/common/command-palette.tsx | 394 ++++++++++-------- .../src/components/common/profile-drawer.tsx | 75 ++++ .../core/src/components/layout/app-header.tsx | 2 +- .../core/src/components/layout/app-layout.tsx | 11 +- .../navigation/mobile-bottom-nav.tsx | 166 ++++++++ .../src/components/navigation/user-nav.tsx | 59 +-- packages/core/src/config/navigation.ts | 102 +++++ packages/core/src/hooks/use-app-hotkeys.ts | 2 +- packages/core/src/hooks/use-drawer-history.ts | 27 ++ .../core/src/stores/profile-drawer-store.ts | 15 + 13 files changed, 644 insertions(+), 225 deletions(-) create mode 100644 packages/core/src/components/common/profile-drawer.tsx create mode 100644 packages/core/src/components/navigation/mobile-bottom-nav.tsx create mode 100644 packages/core/src/hooks/use-drawer-history.ts create mode 100644 packages/core/src/stores/profile-drawer-store.ts diff --git a/apps/native/eslint.config.mjs b/apps/native/eslint.config.mjs index 5062ee3..779528f 100644 --- a/apps/native/eslint.config.mjs +++ b/apps/native/eslint.config.mjs @@ -1,4 +1,9 @@ -import { nextJsConfig } from "@workspace/eslint-config/next-js" +import { nextJsConfig } from "@workspace/eslint-config/next-js"; /** @type {import("eslint").Linter.Config} */ -export default nextJsConfig +export default [ + ...nextJsConfig, + { + ignores: ["src-tauri/target/"], + }, +]; diff --git a/apps/native/src/app/[locale]/layout.tsx b/apps/native/src/app/[locale]/layout.tsx index 972deab..a50c151 100644 --- a/apps/native/src/app/[locale]/layout.tsx +++ b/apps/native/src/app/[locale]/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { notFound } from "next/navigation"; import { Geist, Geist_Mono } from "next/font/google"; import { AppLayout } from "./components/AppLayout"; @@ -22,6 +22,10 @@ export const metadata: Metadata = { description: "TNTStack", }; +export const viewport: Viewport = { + viewportFit: "cover", +}; + export function generateStaticParams() { return routing.locales.map((locale) => ({ locale })); } diff --git a/apps/web/app/[locale]/layout.tsx b/apps/web/app/[locale]/layout.tsx index 03ac89b..e1a8c9b 100644 --- a/apps/web/app/[locale]/layout.tsx +++ b/apps/web/app/[locale]/layout.tsx @@ -62,6 +62,7 @@ export const metadata: Metadata = { export const viewport: Viewport = { themeColor: "#000000", + viewportFit: "cover", }; export function generateStaticParams() { diff --git a/packages/core/src/components/common/command-palette.tsx b/packages/core/src/components/common/command-palette.tsx index 2358ccb..9685db6 100644 --- a/packages/core/src/components/common/command-palette.tsx +++ b/packages/core/src/components/common/command-palette.tsx @@ -18,11 +18,20 @@ import { DialogHeader, DialogTitle, } from "@workspace/ui/components/dialog"; +import { + Drawer, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, +} from "@workspace/ui/components/drawer"; import { useCommandPaletteStore } from "@workspace/core/stores/command-palette-store"; import { useHotkeysDialogStore } from "@workspace/core/stores/hotkeys-store"; import { useSidebarStore } from "@workspace/core/stores/sidebar-store"; import { useThemeStore } from "@workspace/core/stores/theme-store"; import { useThemeTransition } from "@workspace/core/hooks/use-theme-transition"; +import { useIsMobile } from "@workspace/ui/hooks/use-mobile"; +import { useDrawerHistory } from "@workspace/core/hooks/use-drawer-history"; import { useSidebar } from "@workspace/ui/components/sidebar"; import { useLanguageSwitcher } from "@workspace/core/hooks/use-language-switcher"; import { routing, localeConfig } from "@workspace/i18n/routing"; @@ -63,10 +72,8 @@ function CommandMenuKbd({ className, ...props }: React.ComponentProps<"kbd">) { function CommandMenuItem({ children, className, - onHighlight, ...props }: React.ComponentProps & { - onHighlight?: () => void; "data-selected"?: string; "aria-selected"?: string; }) { @@ -90,6 +97,8 @@ export function CommandPalette({ }) { const t = useTranslations("CommandPalette"); const { isOpen, close } = useCommandPaletteStore(); + useDrawerHistory(isOpen, close); + const isMobile = useIsMobile(); const { theme: activeMode, resolvedTheme, @@ -104,7 +113,9 @@ export function CommandPalette({ const runCommand = useCallback( (command: () => unknown) => { close(); - command(); + setTimeout(() => { + command(); + }, 300); }, [close], ); @@ -133,195 +144,216 @@ export function CommandPalette({ const groupClasses = "p-0! **:[[cmdk-group-heading]]:scroll-mt-16 **:[[cmdk-group-heading]]:p-3! **:[[cmdk-group-heading]]:pb-1!"; - return ( - !open && close()}> - - - {t("commandPalette")} - {t("search")} - - - - - - - {t("noResults")} - + const paletteContent = ( + <> + + + + + {t("noResults")} + - - - runCommand(() => - handleThemeChange( - (activeMode === "dark" ? "light" : "dark") as any, - ), - ) - } - > - {activeMode === "dark" ? : } - {t("toggleMode")} - {getKeysDisplay("toggle-mode")} - - runCommand(() => toggleSidebar())} - > - - {t("toggleSidebar")} - {getKeysDisplay("toggle-sidebar")} - - runCommand(() => toggleHotkeysDialog())} - > - - {t("showHotkeys")} - {getKeysDisplay("show-hotkeys")} - - + + + runCommand(() => + handleThemeChange( + (activeMode === "dark" ? "light" : "dark") as "light" | "dark" + ), + ) + } + > + {activeMode === "dark" ? : } + {t("toggleMode")} + {getKeysDisplay("toggle-mode")} + + runCommand(() => toggleSidebar())}> + + {t("toggleSidebar")} + {getKeysDisplay("toggle-sidebar")} + + runCommand(() => toggleHotkeysDialog())} + > + + {t("showHotkeys")} + {getKeysDisplay("show-hotkeys")} + + - + - - runCommand(() => navigate("/home"))} - > - - {t("goHome")} - {getKeysDisplay("go-home")} - - runCommand(() => navigate("/dashboard"))} - > - - {t("goDashboard")} - {getKeysDisplay("go-dashboard")} - - - runCommand(() => navigate("/dashboard/analytics")) - } - > - - {t("goAnalytics")} - {getKeysDisplay("go-analytics")} - - - runCommand(() => navigate("/dashboard/overview")) - } - > - - {t("goOverview")} - {getKeysDisplay("go-overview")} - - runCommand(() => navigate("/settings"))} - > - - {t("goSettings")} - {getKeysDisplay("go-settings")} - - + + runCommand(() => navigate("/home"))} + > + + {t("goHome")} + {getKeysDisplay("go-home")} + + runCommand(() => navigate("/dashboard"))} + > + + {t("goDashboard")} + {getKeysDisplay("go-dashboard")} + + + runCommand(() => navigate("/dashboard/analytics")) + } + > + + {t("goAnalytics")} + {getKeysDisplay("go-analytics")} + + runCommand(() => navigate("/dashboard/overview"))} + > + + {t("goOverview")} + {getKeysDisplay("go-overview")} + + runCommand(() => navigate("/settings"))} + > + + {t("goSettings")} + {getKeysDisplay("go-settings")} + + - + - - {routing.locales.map((loc) => { - const config = localeConfig[loc as keyof typeof localeConfig]; - return ( - - runCommand(() => changeLanguage(loc as any)) - } - disabled={isPending} - > - {config.flag} - {config.nativeName} - {locale === loc && } - - ); - })} - + + {routing.locales.map((loc) => { + const config = localeConfig[loc as keyof typeof localeConfig]; + return ( + runCommand(() => changeLanguage(loc))} + disabled={isPending} + > + {config.flag} + {config.nativeName} + {locale === loc && } + + ); + })} + - + - - {(["sidebar", "floating", "inset"] as const).map( - (sidebarVariant) => ( - - runCommand(() => setVariant(sidebarVariant)) - } - > - - {t(sidebarVariant)} - {variant === sidebarVariant && ( - - )} - - ), - )} - + + {(["sidebar", "floating", "inset"] as const).map( + (sidebarVariant) => ( + runCommand(() => setVariant(sidebarVariant))} + > + + {t(sidebarVariant)} + {variant === sidebarVariant && ( + + )} + + ), + )} + - + - - {themes.map((themeItem) => { - const palette = - (activeMode === "system" ? resolvedTheme : activeMode) === - "dark" - ? themeItem.darkPalette - : themeItem.lightPalette; - return ( - - runCommand(() => setSelectedTheme(themeItem.name)) - } - > - {selectedTheme === themeItem.name ? : } - {themeItem.label} -
- {palette.slice(0, 5).map((color, i) => ( -
- ))} -
- - ); - })} - - - + + {themes.map((themeItem) => { + const palette = + (activeMode === "system" ? resolvedTheme : activeMode) === + "dark" + ? themeItem.darkPalette + : themeItem.lightPalette; + return ( + + runCommand(() => setSelectedTheme(themeItem.name)) + } + > + {selectedTheme === themeItem.name ? : } + {themeItem.label} +
+ {palette.slice(0, 5).map((color, i) => ( +
+ ))} +
+ + ); + })} + + + -
-
- - - - - - - {t("navigate")} - - - - {t("openOrSelect")} -
-
- Esc - {t("close")} -
+
+
+ + + + + + + {t("navigate")} + + + + {t("openOrSelect")}
+
+ Esc + {t("close")} +
+
+ + ); + + if (isMobile) { + return ( + !open && close()}> + e.preventDefault()} + > + + {t("commandPalette")} + {t("search")} + + {paletteContent} + + + ); + } + + return ( + !open && close()}> + + + {t("commandPalette")} + {t("search")} + + {paletteContent} ); diff --git a/packages/core/src/components/common/profile-drawer.tsx b/packages/core/src/components/common/profile-drawer.tsx new file mode 100644 index 0000000..84f6914 --- /dev/null +++ b/packages/core/src/components/common/profile-drawer.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useTranslations } from "@workspace/i18n"; +import { + Drawer, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerDescription, +} from "@workspace/ui/components/drawer"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@workspace/ui/components/avatar"; +import { Separator } from "@workspace/ui/components/separator"; +import { Button } from "@workspace/ui/components/button"; +import { useDrawerHistory } from "@workspace/core/hooks/use-drawer-history"; +import { useProfileDrawerStore } from "@workspace/core/stores/profile-drawer-store"; +import { UserNavItem, navigationData } from "@workspace/core/config/navigation"; + +interface ProfileDrawerProps { + user: UserNavItem; +} + +export function ProfileDrawer({ user }: ProfileDrawerProps) { + const { isOpen, setOpen } = useProfileDrawerStore(); + useDrawerHistory(isOpen, setOpen); + const t = useTranslations("Navigation"); + + return ( + + + + {t("account")} + {user.name} + +
+ + + + {user.name.slice(0, 2).toUpperCase()} + + +
+ {user.name} + {user.email} +
+
+ +
+ {navigationData.navProfile.map((group, index) => ( +
+ {group.items.map((item) => { + const Icon = item.icon; + return ( + + ); + })} + {index < navigationData.navProfile.length - 1 && } +
+ ))} +
+
+
+ ); +} diff --git a/packages/core/src/components/layout/app-header.tsx b/packages/core/src/components/layout/app-header.tsx index 7dd3319..97c8c91 100644 --- a/packages/core/src/components/layout/app-header.tsx +++ b/packages/core/src/components/layout/app-header.tsx @@ -41,7 +41,7 @@ export function AppHeader({ pathname, LinkComponent = "a" }: AppHeaderProps) { const isHome = pathname === "/home" || pathname === "/"; return ( -
+
- + {children} + + ); diff --git a/packages/core/src/components/navigation/mobile-bottom-nav.tsx b/packages/core/src/components/navigation/mobile-bottom-nav.tsx new file mode 100644 index 0000000..673be9a --- /dev/null +++ b/packages/core/src/components/navigation/mobile-bottom-nav.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { ComponentType } from "react"; +import { type LucideIcon } from "lucide-react"; +import { motion, AnimatePresence } from "motion/react"; +import { cn } from "@workspace/ui/lib/utils"; +import { useTranslations } from "@workspace/i18n"; +import { useProfileDrawerStore } from "@workspace/core/stores/profile-drawer-store"; +import { useCommandPaletteStore } from "@workspace/core/stores/command-palette-store"; +import { BorderBeam } from "@workspace/ui/components/landing/border-beam"; + +export interface MobileBottomNavItem { + title: string; + url: string; + icon: LucideIcon; + isActive?: boolean; + translationKey: string; +} + +export interface MobileBottomNavProps { + items: MobileBottomNavItem[]; + pathname: string; + className?: string; + LinkComponent?: + | ComponentType<{ + href: string; + children: React.ReactNode; + onClick?: () => void; + className?: string; + }> + | "a"; +} + +const buttonVariants = { + initial: { gap: 0 }, + animate: (isSelected: boolean) => ({ + gap: isSelected ? ".5rem" : 0, + }), +}; + +const spanVariants = { + initial: { width: 0, opacity: 0 }, + animate: { width: "auto", opacity: 1 }, + exit: { width: 0, opacity: 0 }, +}; + +const transition = { + delay: 0.1, + type: "spring" as const, + bounce: 0, + duration: 0.6, +}; + +export function MobileBottomNav({ + items, + pathname, + className, + LinkComponent = "a", +}: MobileBottomNavProps) { + const t = useTranslations("Navigation"); + const { open: openCommandPalette } = useCommandPaletteStore(); + const { open: openProfileDrawer } = useProfileDrawerStore(); + + return ( + + ); +} diff --git a/packages/core/src/components/navigation/user-nav.tsx b/packages/core/src/components/navigation/user-nav.tsx index 4ecaaad..4ebe37e 100644 --- a/packages/core/src/components/navigation/user-nav.tsx +++ b/packages/core/src/components/navigation/user-nav.tsx @@ -1,14 +1,7 @@ "use client"; -import { - BadgeCheck, - Bell, - ChevronsUpDown, - CreditCard, - LogOut, - Sparkles, -} from "lucide-react"; - +import { ChevronsUpDown } from "lucide-react"; +import { navigationData } from "@workspace/core/config/navigation"; import { Avatar, AvatarFallback, @@ -27,7 +20,6 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, - useSidebar, } from "@workspace/ui/components/sidebar"; import { useTranslations } from "@workspace/i18n"; @@ -42,7 +34,6 @@ interface UserNavProps { } export function UserNav({ user }: UserNavProps) { - const { isMobile } = useSidebar(); const t = useTranslations("Navigation"); return ( @@ -69,7 +60,7 @@ export function UserNav({ user }: UserNavProps) { @@ -88,32 +79,24 @@ export function UserNav({ user }: UserNavProps) {
- - - - {t("upgradeToPro")} - - - - - - - {t("account")} - - - - {t("billing")} - - - - {t("notifications")} - - - - - - {t("logOut")} - + {navigationData.navProfile.map((group, index) => ( +
+ + {group.items.map((item) => { + const Icon = item.icon; + return ( + + + {t(item.translationKey as Parameters[0])} + + ); + })} + + {index < navigationData.navProfile.length - 1 && ( + + )} +
+ ))} diff --git a/packages/core/src/config/navigation.ts b/packages/core/src/config/navigation.ts index 4a07b17..d4586d1 100644 --- a/packages/core/src/config/navigation.ts +++ b/packages/core/src/config/navigation.ts @@ -1,9 +1,16 @@ import { Home, Send, + User, + Bell, + LogOut, + Search, Github, + Sparkles, Settings, PieChart, + BadgeCheck, + CreditCard, LucideIcon, LayoutDashboard, } from "lucide-react"; @@ -29,6 +36,13 @@ export interface MainNavItem { translationKey: string; } +export interface MobileNavItem { + title: string; + url: string; + icon: LucideIcon; + translationKey: string; +} + export interface SecondaryNavItem { title: string; url: string; @@ -44,11 +58,25 @@ export interface ProjectNavItem { translationKey: string; } +export interface ProfileNavItem { + title: string; + url: string; + icon: LucideIcon; + translationKey: string; +} + +export interface ProfileNavGroup { + id: string; + items: ProfileNavItem[]; +} + export interface NavigationData { user: UserNavItem; navMain: MainNavItem[]; + navMobile: MobileNavItem[]; navSecondary: SecondaryNavItem[]; projects: ProjectNavItem[]; + navProfile: ProfileNavGroup[]; } export const navigationData: NavigationData = { @@ -86,6 +114,33 @@ export const navigationData: NavigationData = { ], }, ], + navMobile: [ + { title: "Home", url: "/home", icon: Home, translationKey: "home" }, + { + title: "Search", + url: "#search", + icon: Search, + translationKey: "search", + }, + { + title: "Dashboard", + url: "/dashboard", + icon: LayoutDashboard, + translationKey: "dashboard", + }, + { + title: "Profile", + url: "#profile", + icon: User, + translationKey: "profile", + }, + { + title: "Settings", + url: "/settings", + icon: Settings, + translationKey: "settings", + }, + ], navSecondary: [ { title: "Feedback", @@ -116,4 +171,51 @@ export const navigationData: NavigationData = { translationKey: "projectAlpha", }, ], + navProfile: [ + { + id: "group-1", + items: [ + { + title: "Upgrade to Pro", + url: "#upgrade", + icon: Sparkles, + translationKey: "upgradeToPro", + }, + ], + }, + { + id: "group-2", + items: [ + { + title: "Account", + url: "#account", + icon: BadgeCheck, + translationKey: "account", + }, + { + title: "Billing", + url: "#billing", + icon: CreditCard, + translationKey: "billing", + }, + { + title: "Notifications", + url: "#notifications", + icon: Bell, + translationKey: "notifications", + }, + ], + }, + { + id: "group-3", + items: [ + { + title: "Log Out", + url: "#logout", + icon: LogOut, + translationKey: "logOut", + }, + ], + }, + ], }; diff --git a/packages/core/src/hooks/use-app-hotkeys.ts b/packages/core/src/hooks/use-app-hotkeys.ts index f4f8308..d2e2392 100644 --- a/packages/core/src/hooks/use-app-hotkeys.ts +++ b/packages/core/src/hooks/use-app-hotkeys.ts @@ -34,7 +34,7 @@ export function useAppHotkeys({ navigate }: UseAppHotkeysOptions) { getKeys("toggle-mode"), (e: KeyboardEvent) => { e.preventDefault(); - handleThemeChange((theme === "dark" ? "light" : "dark") as any); + handleThemeChange((theme === "dark" ? "light" : "dark") as "light" | "dark"); }, { enableOnFormTags: false }, ); diff --git a/packages/core/src/hooks/use-drawer-history.ts b/packages/core/src/hooks/use-drawer-history.ts new file mode 100644 index 0000000..d6bc38a --- /dev/null +++ b/packages/core/src/hooks/use-drawer-history.ts @@ -0,0 +1,27 @@ +"use client"; + +import { useEffect } from "react"; + +export function useDrawerHistory( + isOpen: boolean, + onOpenChange: (open: boolean) => void, +) { + useEffect(() => { + if (!isOpen) return; + + window.history.pushState({ drawer: "open" }, ""); + + const handlePopState = () => { + onOpenChange(false); + }; + + window.addEventListener("popstate", handlePopState); + + return () => { + window.removeEventListener("popstate", handlePopState); + if (window.history.state?.drawer === "open") { + window.history.back(); + } + }; + }, [isOpen, onOpenChange]); +} diff --git a/packages/core/src/stores/profile-drawer-store.ts b/packages/core/src/stores/profile-drawer-store.ts new file mode 100644 index 0000000..5dfc022 --- /dev/null +++ b/packages/core/src/stores/profile-drawer-store.ts @@ -0,0 +1,15 @@ +import { create } from "zustand"; + +interface ProfileDrawerStore { + isOpen: boolean; + setOpen: (open: boolean) => void; + open: () => void; + close: () => void; +} + +export const useProfileDrawerStore = create((set) => ({ + isOpen: false, + setOpen: (isOpen) => set({ isOpen }), + open: () => set({ isOpen: true }), + close: () => set({ isOpen: false }), +})); From dda740b7322b0c84d3d2e3f943872d6c8b0b8252 Mon Sep 17 00:00:00 2001 From: dest Date: Thu, 23 Apr 2026 15:02:58 +0300 Subject: [PATCH 2/5] refactor(ui): implement adaptive sizing and typography via CSS variables --- packages/ui/src/components/avatar.tsx | 20 ++-- packages/ui/src/components/breadcrumb.tsx | 37 +++---- packages/ui/src/components/button.tsx | 31 +++--- packages/ui/src/components/card.tsx | 26 ++--- packages/ui/src/components/collapsible.tsx | 12 +-- packages/ui/src/components/command.tsx | 14 +-- packages/ui/src/components/dialog.tsx | 56 ++++++----- packages/ui/src/components/drawer.tsx | 46 +++++---- packages/ui/src/components/dropdown-menu.tsx | 78 +++++++-------- packages/ui/src/components/input.tsx | 12 +-- packages/ui/src/components/kbd.tsx | 12 +-- packages/ui/src/components/label.tsx | 16 +-- packages/ui/src/components/scroll-area.tsx | 16 +-- packages/ui/src/components/select.tsx | 57 ++++++----- packages/ui/src/components/separator.tsx | 14 +-- packages/ui/src/components/sheet.tsx | 50 ++++++---- packages/ui/src/components/sidebar.tsx | 100 +++++++++++-------- packages/ui/src/components/skeleton.tsx | 6 +- packages/ui/src/components/tooltip.tsx | 22 ++-- packages/ui/src/styles/globals.css | 40 ++++++++ 20 files changed, 376 insertions(+), 289 deletions(-) diff --git a/packages/ui/src/components/avatar.tsx b/packages/ui/src/components/avatar.tsx index 5f3a8f7..2cf6dd8 100644 --- a/packages/ui/src/components/avatar.tsx +++ b/packages/ui/src/components/avatar.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; -import { cn } from "@workspace/ui/lib/utils" +import { cn } from "@workspace/ui/lib/utils"; function Avatar({ className, @@ -14,11 +14,11 @@ function Avatar({ data-slot="avatar" className={cn( "relative flex size-8 shrink-0 overflow-hidden rounded-full", - className + className, )} {...props} /> - ) + ); } function AvatarImage({ @@ -31,7 +31,7 @@ function AvatarImage({ className={cn("aspect-square size-full", className)} {...props} /> - ) + ); } function AvatarFallback({ @@ -43,11 +43,11 @@ function AvatarFallback({ data-slot="avatar-fallback" className={cn( "bg-muted flex size-full items-center justify-center rounded-full", - className + className, )} {...props} /> - ) + ); } -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/packages/ui/src/components/breadcrumb.tsx b/packages/ui/src/components/breadcrumb.tsx index 33784bf..7bf8f72 100644 --- a/packages/ui/src/components/breadcrumb.tsx +++ b/packages/ui/src/components/breadcrumb.tsx @@ -1,11 +1,11 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; -import { cn } from "@workspace/ui/lib/utils" +import { cn } from "@workspace/ui/lib/utils"; function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { - return