chore(ux): basic navigation functions

This commit is contained in:
icld
2025-03-06 13:44:09 -08:00
parent 08cb749755
commit 917787aac4
65 changed files with 1248 additions and 1855 deletions
@@ -1,217 +0,0 @@
// 'use client';
// import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
// import { LaconicMark } from '@/laconic-assets/laconic-mark';
// import { Menu, Plus, Shapes, Wallet } from 'lucide-react';
// import Link from 'next/link';
// import { useRouter } from 'next/navigation';
// import { useState } from 'react';
// import { ProjectSearchBar } from '../search/ProjectSearchBar';
// import { ColorModeToggle } from './components/ColorModeToggle';
// import { GitHubSessionButton } from './components/GitHubSessionButton';
// import { WalletSessionId } from './components/WalletSessionId';
// import { Button } from '@workspace/ui/components/button';
// /**
// * TopNavigation Component
// *
// * Renders the top navigation bar, adapting its layout for desktop and mobile views.
// * It includes the project search bar, navigation links, user authentication via GitHub,
// * color mode toggle, and wallet session information. On mobile, it utilizes a sheet
// * for a responsive and accessible navigation experience.
// */
// export function TopNavigation() {
// const router = useRouter();
// const [showOnboarding, setShowOnboarding] = useState(false);
// // In a real implementation, these would be from context/hooks
// const isGitHubConnected = true; // Example value
// const defaultOrg = { slug: 'demo-org' }; // Example value
// const isReady = true; // Wallet ready status
// const handleOnboardingClose = () => {
// setShowOnboarding(false);
// // In a real implementation, would refresh organization data after onboarding
// };
// // Navigate to create page with organization slug
// const handleCreateNew = () => {
// if (defaultOrg) {
// router.push(`/${defaultOrg.slug}/ps/cr`);
// } else {
// // If no organization is available, show onboarding
// setShowOnboarding(true);
// }
// };
// const handleRunOnboarding = () => {
// // Clear existing onboarding progress data
// if (typeof window !== 'undefined') {
// localStorage.removeItem('onboarding_progress');
// localStorage.removeItem('onboarding_state');
// // Force starting from connect step
// localStorage.setItem('onboarding_force_connect', 'true');
// }
// setShowOnboarding(true);
// };
// return (
// <div className="bg-background">
// {/* Top Navigation - Desktop */}
// <nav className="md:flex items-center justify-between hidden h-16 px-6 border-b">
// <div className="flex items-center gap-6">
// {/* Logo / Home Link */}
// <Link href="/" className="flex items-center justify-center w-10 h-10">
// <LaconicMark />
// </Link>
// {/* Navigation Items */}
// <div className="flex items-center gap-4">
// <Link href="/pr">
// <Button variant="ghost" className="gap-2">
// <Shapes className="w-5 h-5" />
// <span>Projects</span>
// </Button>
// </Link>
// <Button
// variant="ghost"
// className="text-muted-foreground gap-2"
// >
// <Link href="/wallet">
// <Wallet className="w-5 h-5" />
// <span>Wallet</span>
// </Link>
// </Button>
// </div>
// </div>
// <div className="flex items-center gap-4">
// {/* Add New Button with Dropdown */}
// <DropdownMenu>
// <DropdownMenuTrigger asChild>
// <Button variant="ghost" size="icon" className="rounded-full">
// <Plus className="h-5 w-5" />
// </Button>
// </DropdownMenuTrigger>
// <DropdownMenuContent align="end">
// {isGitHubConnected && (
// <>
// <DropdownMenuItem onClick={handleCreateNew}>
// Create New
// </DropdownMenuItem>
// <DropdownMenuSeparator />
// </>
// )}
// <DropdownMenuItem onClick={handleRunOnboarding}>
// Run Onboarding
// </DropdownMenuItem>
// </DropdownMenuContent>
// </DropdownMenu>
// <Button variant="ghost" className="text-muted-foreground">
// <Link href="/support">Support</Link>
// </Button>
// <Button variant="ghost" className="text-muted-foreground">
// <Link href="/docs">Documentation</Link>
// </Button>
// <GitHubSessionButton />
// <ColorModeToggle />
// <WalletSessionId walletId="0xAb...1234" />
// </div>
// </nav>
// {/* Top Navigation - Mobile */}
// <nav className="md:hidden flex items-center justify-between h-16 px-4 border-b">
// <Sheet>
// <SheetTrigger>
// <Button variant="outline" size="icon">
// <Menu className="w-4 h-4" />
// </Button>
// </SheetTrigger>
// <SheetContent
// side="left"
// className="w-[300px] sm:w-[400px] flex flex-col"
// >
// <nav className="flex flex-col flex-grow space-y-4">
// <div className="px-4 py-2">
// <ProjectSearchBar
// onChange={(project) => {
// router.push(
// `/pr/${project.organization.slug}/ps/${project.id}`,
// );
// }}
// />
// </div>
// <Button variant="ghost" className="justify-start gap-2">
// <Link href="/pr">
// <Shapes className="w-5 h-5" />
// <span>Projects</span>
// </Link>
// </Button>
// <Button variant="ghost" className="justify-start gap-2">
// <Link href="/wallet">
// <Wallet className="w-5 h-5" />
// <span>Wallet</span>
// </Link>
// </Button>
// <Button variant="ghost" className="justify-start">
// <Link href="/support">Support</Link>
// </Button>
// <Button variant="ghost" className="justify-start">
// <Link href="/docs">Documentation</Link>
// </Button>
// <Button
// variant="ghost"
// className="justify-start"
// onClick={handleRunOnboarding}
// >
// Run Onboarding
// </Button>
// {isGitHubConnected && (
// <Button
// variant="ghost"
// className="justify-start"
// onClick={handleCreateNew}
// >
// Create New
// </Button>
// )}
// </nav>
// <div className="flex items-center justify-between mt-auto">
// <GitHubSessionButton />
// <ColorModeToggle />
// <WalletSessionId walletId="0xAb...1234" />
// </div>
// </SheetContent>
// </Sheet>
// <div className="flex items-center gap-2">
// {/* Add New Button (Mobile) */}
// <DropdownMenu>
// <DropdownMenuTrigger asChild>
// <Button variant="ghost" size="icon" className="rounded-full">
// <Plus className="h-5 w-5" />
// </Button>
// </DropdownMenuTrigger>
// <DropdownMenuContent align="end">
// {isGitHubConnected && (
// <>
// <DropdownMenuItem onClick={handleCreateNew}>
// Create New
// </DropdownMenuItem>
// <DropdownMenuSeparator />
// </>
// )}
// <DropdownMenuItem onClick={handleRunOnboarding}>
// Run Onboarding
// </DropdownMenuItem>
// </DropdownMenuContent>
// </DropdownMenu>
// <ColorModeToggle />
// <WalletSessionId walletId="0xAb...1234" />
// </div>
// </nav>
// </div>
// );
// }
@@ -0,0 +1,128 @@
'use client'
import PageWrapper from '@/components/foundation/page-wrapper'
import { Button } from '@workspace/ui/components/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle
} from '@workspace/ui/components/card'
/**
* BuyPrepaidService component allows users to buy prepaid services.
* It checks if the user's balance is sufficient and redirects them to the home page if it is.
*
* @returns {JSX.Element} A JSX element that renders the buy prepaid service page.
*/
const BuyPrepaidService = () => {
return (
<PageWrapper
// header={{
// title: 'Buy prepaid service',
// actions: [
// {
// label: 'Buy prepaid service',
// href: process.env.NEXT_PUBLIC_SHOPIFY_APP_URL,
// isPrimary: true
// }
// ]
// }}
>
<div className=" mx-auto px-4 py-16 md:py-18 w-full">
<div className="mx-auto w-full min-w-fit">
<div className="text-center mb-12">
<h2 className="font-normal text-gray-400 tracking-tight sm:text-2xl text-foreground">
Laconic
</h2>
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl text-foreground">
Webapp Deployment Plans
</h1>
<p className="mt-4 text-muted-foreground">
Choose the perfect deployment plan for your needs. Scale your
applications with confidence.
</p>
</div>
<div className="grid gap-6 md:grid-cols-3 w-full">
{/* Basic Plan */}
<Card className="border border-gray-200">
<CardHeader>
<CardTitle>Basic</CardTitle>
<CardDescription>
A simple deployment option for small projects
</CardDescription>
<div className="mt-4">
<span className="text-3xl font-bold">$5.00</span>
<span className="text-muted-foreground ml-1">/month</span>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-6">
1 monthly webapp deployment
</p>
</CardContent>
<CardFooter>
<Button className="w-full" variant="outline">
Purchase plan
</Button>
</CardFooter>
</Card>
{/* Standard Plan */}
<Card className="border-2 border-primary relative">
<div className="absolute -top-3 left-0 right-0 mx-auto w-fit rounded-full bg-primary px-3 py-1 text-xs font-medium text-primary-foreground">
Most popular
</div>
<CardHeader>
<CardTitle>Standard</CardTitle>
<CardDescription>
Perfect for growing projects and businesses
</CardDescription>
<div className="mt-4">
<span className="text-3xl font-bold">$50.00</span>
<span className="text-muted-foreground ml-1">/month</span>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-6">
10 monthly webapp deployments
</p>
</CardContent>
<CardFooter>
<Button className="w-full">Purchase plan</Button>
</CardFooter>
</Card>
{/* Premium Plan */}
<Card className="border border-gray-200">
<CardHeader>
<CardTitle>Premium</CardTitle>
<CardDescription>
For enterprises with high-volume needs
</CardDescription>
<div className="mt-4">
<span className="text-3xl font-bold">$500.00</span>
<span className="text-muted-foreground ml-1">/month</span>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-6">
100 monthly webapp deployments
</p>
</CardContent>
<CardFooter>
<Button className="w-full" variant="outline">
Purchase plan
</Button>
</CardFooter>
</Card>
</div>
</div>
</div>
</PageWrapper>
)
}
export default BuyPrepaidService
@@ -1,4 +1,5 @@
import type { Metadata } from 'next'
import BuyPrepaidService from './BuyServices'
export const metadata: Metadata = {
title: 'Buy Prepaid Service',
@@ -6,13 +7,7 @@ export const metadata: Metadata = {
}
const Page = () => {
return (
<div>
<h1>
Hello from (web3-authenticated)/(dashboard)/(buy-prepaid-service)/bp
</h1>
</div>
)
return <BuyPrepaidService />
}
export default Page
@@ -1,8 +1,7 @@
'use client'
import { DashboardLayout } from '@/components/layouts'
import { defaultDashboardConfig } from '@/config/dashboard'
import type React from 'react'
import NavigationWrapper from '@/components/foundation/navigation-wrapper'
import { TopNavigation } from '@/components/foundation/top-navigation/top-navigation'
interface LayoutProps {
children: React.ReactNode
@@ -10,13 +9,10 @@ interface LayoutProps {
const Layout: React.FC<LayoutProps> = ({ children }) => {
return (
// <div className="flex flex-col min-h-screen">
// <div className="flex-1 p-8">
<DashboardLayout config={defaultDashboardConfig}>
<NavigationWrapper>
<TopNavigation />
{children}
</DashboardLayout>
// </div>
// </div>
</NavigationWrapper>
)
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+2 -10
View File
@@ -1,4 +1,4 @@
import { Providers } from '@/components/providers'
import { Providers } from '@/components/foundation/providers'
// import '@workspace/ui/globals.css'
import type { Metadata } from 'next'
@@ -26,16 +26,8 @@ export default function RootLayout({
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${inter.className} `}>
<body className={`${inter.className} `} suppressHydrationWarning>
<main>
<button
type="button"
// onClick={() => {
// console.log('clicked')
// }}
>
Click me
</button>
<Providers>{children}</Providers>
</main>
</body>
+5
View File
@@ -0,0 +1,5 @@
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
export default function Loading() {
return <LoadingOverlay isLoading={true} />
}
+51 -10
View File
@@ -2,21 +2,62 @@ import type { Metadata } from 'next'
import Link from 'next/link'
export const metadata: Metadata = {
title: 'Page Not Found',
description: 'The requested page could not be found'
title: '404',
description: 'Page not found'
}
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-screen p-4">
<h2 className="text-2xl font-bold mb-4">404 - Page Not Found</h2>
<p className="mb-8">The page you are looking for does not exist.</p>
<Link
href="/"
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Return Home
</Link>
<div className="text-center">
<p className="text-gray-500 mb-2">404</p>
<h1 className="text-3xl font-bold mb-4">Page not found</h1>
<p className="text-gray-600 mb-8">
Add a brief, empathetic explanation that acknowledges the error while
maintaining brand voice. Keep it light and helpful.
</p>
<div className="space-y-4 max-w-md mx-auto">
<Link
href="/"
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
>
<div>
<div className="font-medium">Go back to homepage</div>
<div className="text-gray-600 text-sm">
Lorem ipsum dolor sit amet, consectetur adipisci.
</div>
</div>
<span className="text-gray-400"></span>
</Link>
<Link
href="/docs"
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
>
<div>
<div className="font-medium">Read our documentation</div>
<div className="text-gray-600 text-sm">
Lorem ipsum dolor sit amet, consectetur adipisci.
</div>
</div>
<span className="text-gray-400"></span>
</Link>
<Link
href="/blog"
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
>
<div>
<div className="font-medium">Explore our blog</div>
<div className="text-gray-600 text-sm">
Lorem ipsum dolor sit amet, consectetur adipisci.
</div>
</div>
<span className="text-gray-400"></span>
</Link>
</div>
</div>
</div>
)
}
+17 -1
View File
@@ -1,3 +1,4 @@
import PageWrapper from '@/components/foundation/page-wrapper'
import { Button } from '@workspace/ui/components/button'
import type { Metadata } from 'next'
@@ -7,5 +8,20 @@ export const metadata: Metadata = {
}
export default function Page() {
return <Button size="sm">Button</Button>
return (
<PageWrapper>
<section className="flex flex-col items-center text-center max-w-3xl mx-auto py-16 space-y-6">
<div className="text-sm font-medium">Hero section</div>
<h1 className="text-4xl md:text-5xl font-bold">
Headline that solves user's main problem
</h1>
<p className="text-lg text-muted-foreground">
Follow with one or two sentences that expand on your value
proposition. Focus on key benefits and address why users should take
action now. Keep it scannable, short and benefit-driven.
</p>
<Button size="lg">Get started</Button>
</section>
</PageWrapper>
)
}
@@ -0,0 +1,43 @@
/**
* The Laconic logo mark component.
*
* @component
* @example
* // Basic usage
* <LaconicMark />
*
* // With custom color
* <LaconicMark className="text-blue-500" />
*
* // With custom size
* <LaconicMark className="w-12 h-12" />
*/
export function LaconicMark({
/**
* Optional className for styling the SVG
* Use this to customize the size, color, and other SVG properties
*/
className = ''
}: {
className?: string
}) {
return (
<svg
width="80"
height="81"
viewBox="0 0 80 81"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-label="Laconic logo"
>
<title>Laconic logo</title>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M23.4359 41.7985C29.255 35.9796 32.8569 27.9437 32.8552 19.0713C32.8574 17.867 32.7909 16.6761 32.6587 15.5L15 15.5016L15.0005 49.4307C14.9989 53.5435 16.567 57.6575 19.7035 60.7938C22.8402 63.9303 26.9571 65.5003 31.0705 65.4982L65 65.5L64.9987 47.8387C63.8252 47.7095 62.6342 47.6425 61.4272 47.6423C52.5572 47.6435 44.521 51.245 38.7019 57.0638C34.4676 61.192 27.6786 61.1927 23.4965 57.0107C19.3172 52.8312 19.3153 46.0402 23.4359 41.7985ZM61.3372 19.1716C56.4605 14.2952 48.5377 14.2933 43.6595 19.1716C38.7812 24.0496 38.7831 31.9724 43.6595 36.8485C48.5387 41.7275 56.459 41.7267 61.3372 36.8485C66.2155 31.9705 66.2165 24.0504 61.3372 19.1716Z"
fill="currentColor"
/>
</svg>
)
}
@@ -0,0 +1,83 @@
'use client'
import { LaconicMark } from '@/components/assets/laconic-mark'
import { cn } from '@workspace/ui/lib/utils'
import { Loader2 } from 'lucide-react'
export interface LoadingOverlayProps {
/**
* Controls the visibility of the overlay.
* When false, the component returns null.
* @default true
*/
isLoading?: boolean
/**
* Optional className for styling the overlay container.
* This will be merged with the default styles.
*/
className?: string
/**
* Whether to show the Laconic logo in the overlay.
* @default true
*/
showLogo?: boolean
/**
* Whether to show the loading spinner below the logo.
* @default true
*/
showSpinner?: boolean
/**
* The z-index value for the overlay.
* Adjust this if you need the overlay to appear above or below other elements.
* @default 50
*/
zIndex?: number
/**
* Whether to use solid black background instead of semi-transparent.
* Useful for initial page load and full-screen loading states.
* @default false
*/
solid?: boolean
}
export function LoadingOverlay({
isLoading = true,
className,
showLogo = true,
showSpinner = true,
zIndex = 50,
solid = false
}: LoadingOverlayProps) {
if (!isLoading) return null
return (
<div
className={cn(
'fixed inset-0 flex flex-col items-center justify-center',
solid ? 'bg-black' : 'bg-black/90 backdrop-blur-sm',
className
)}
style={{ zIndex }}
role="alert"
aria-busy="true"
aria-label="Loading"
>
{showLogo && (
<div className="animate-fade-in mb-4">
<LaconicMark className="text-white" />
</div>
)}
{showSpinner && (
<Loader2
className="animate-spin w-6 h-6 text-white"
aria-hidden="true"
/>
)}
</div>
)
}
@@ -0,0 +1,74 @@
import { cn } from '@workspace/ui/lib/utils'
import type { NavigationWrapperProps } from './types'
/**
* NavigationWrapper component
*
* Provides a layout wrapper with a top navigation bar.
* This component is intended to be used at the layout level,
* wrapping the entire application or major sections of it.
*
* @example
* ```tsx
* // In app/layout.tsx
* export default function RootLayout({ children }) {
* return (
* <html lang="en">
* <body>
* <NavigationWrapper>
* {children}
* </NavigationWrapper>
* </body>
* </html>
* );
* }
* ```
*
* @param props - The component props
* @returns A layout wrapper with top navigation
*/
export default function NavigationWrapper({
children,
className
}: NavigationWrapperProps) {
return (
<div className={cn('flex flex-col min-h-screen w-full', className)}>
{/* Top Navigation */}
{/* <header className="sticky top-0 z-40 w-full border-b bg-background">
<div className="container flex h-16 items-center">
<div className="mr-4 flex">
<Link href="/" className="flex items-center space-x-2">
<span className="font-bold text-xl">App Logo</span>
</Link>
</div>
<nav className="flex items-center space-x-4 lg:space-x-6 mx-6">
<Link
href="/dashboard"
className="text-sm font-medium transition-colors hover:text-primary"
> the fuck
Dashboard
</Link>
<Link
href="/projects"
className="text-sm font-medium transition-colors hover:text-primary"
>
Projects
</Link>
<Link
href="/settings"
className="text-sm font-medium transition-colors hover:text-primary"
>
Settings
</Link>
</nav>
<div className="ml-auto flex items-center space-x-4">
<Button className="text-sm font-medium">Profile</Button>
</div>
</div>
</header> */}
{/* Main Content */}
{children}
</div>
)
}
@@ -0,0 +1,104 @@
'use client'
import { Button } from '@workspace/ui/components/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@workspace/ui/components/dropdown-menu'
import { cn } from '@workspace/ui/lib/utils'
import { MoreVertical } from 'lucide-react'
import Link from 'next/link'
import type { PageAction, PageHeaderProps } from './types'
export default function PageHeader({
title,
description,
actions = [],
className
}: PageHeaderProps) {
// Separate primary actions from secondary actions
const primaryActions = actions.filter((action) => action.isPrimary)
const secondaryActions = actions.filter((action) => !action.isPrimary)
// Render an action (either as button or link)
const renderAction = (action: PageAction, key: string) => {
const variant = action.variant || (action.isPrimary ? 'default' : 'outline')
if (action.href) {
return (
<Button key={key} variant={variant} asChild>
<Link href={action.href}>{action.label}</Link>
</Button>
)
}
return (
<Button key={key} variant={variant} onClick={action.onClick}>
{action.label}
</Button>
)
}
return (
<div className={cn('w-full max-w-[1232px] mx-auto', className)}>
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-6 md:gap-2">
<div className="space-y-2">
<h1 className="text-2xl md:text-[30px] font-bold leading-8 md:leading-9 text-foreground">
{title}
</h1>
{description && (
<p className="text-sm md:text-base text-muted-foreground leading-5 md:leading-6">
{description}
</p>
)}
</div>
{actions.length > 0 && (
<>
{/* Desktop buttons */}
<div className="hidden md:flex items-center gap-2">
{actions.map((action, index) =>
renderAction(action, `desktop-${index}`)
)}
</div>
{/* Mobile buttons */}
<div className="flex md:hidden items-center gap-2">
{primaryActions.map((action, index) =>
renderAction(action, `mobile-primary-${index}`)
)}
{secondaryActions.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{secondaryActions.map((action) =>
action.href ? (
<DropdownMenuItem asChild key={action.label}>
<Link href={action.href}>{action.label}</Link>
</DropdownMenuItem>
) : (
<DropdownMenuItem
onClick={action.onClick}
key={action.label}
>
{action.label}
</DropdownMenuItem>
)
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</>
)}
</div>
</div>
)
}
@@ -0,0 +1,32 @@
import { cn } from '@workspace/ui/lib/utils'
import PageHeader from './page-header'
import type { PageWrapperProps } from './types'
export default function PageWrapper({
header,
children,
layout = 'default',
className,
contentClassName
}: PageWrapperProps) {
return (
<div className={cn('flex flex-col h-full', className)}>
{header && (
<div className="py-6 md:py-8 px-4 md:px-6 border-b">
<PageHeader {...header} />
</div>
)}
<div className={cn('flex-1 px-4 md:px-6 py-6 md:py-8', contentClassName)}>
<div
className={cn(
'mx-auto',
layout === 'default' ? 'max-w-4xl' : 'w-full max-w-[1232px]',
layout === 'bento' && 'grid grid-cols-1 md:grid-cols-3 gap-6'
)}
>
{children}
</div>
</div>
</div>
)
}
@@ -0,0 +1,23 @@
'use client'
import { ThemeProvider } from 'next-themes'
import type * as React from 'react'
import '@workspace/ui/globals.css'
import { Toaster } from 'sonner'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme
>
<>
<Toaster />
{children}
</>
</ThemeProvider>
)
}
@@ -0,0 +1,27 @@
'use client'
import { Button } from '@workspace/ui/components/button'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
export function DarkModeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="rounded-md p-2 hover:bg-accent"
aria-label={
theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'
}
>
{theme === 'dark' ? (
<Sun className="h-5 w-5" />
) : (
<Moon className="h-5 w-5" />
)}
</Button>
)
}
@@ -0,0 +1,80 @@
'use client'
import { Button } from '@workspace/ui/components/button'
import { cn } from '@workspace/ui/lib/utils'
import type { LucideIcon } from 'lucide-react'
import Link from 'next/link'
import type React from 'react'
interface NavigationItemProps {
href?: string
onClick?: () => void
className?: string
active?: boolean
icon?: LucideIcon
children: React.ReactNode
variant?:
| 'default'
| 'destructive'
| 'outline'
| 'secondary'
| 'ghost'
| 'link'
isDrawerItem?: boolean
}
export function NavigationItem({
href,
onClick,
className,
active = false,
icon: Icon,
children,
variant = 'ghost',
isDrawerItem = false
}: NavigationItemProps) {
// For drawer items, use a simpler styling approach
if (isDrawerItem) {
const content = (
<div
className={cn(
'px-6 py-1 text-sm font-medium transition-colors hover:text-white/80',
active && 'text-white font-semibold',
className
)}
>
{children}
</div>
)
if (href) {
return <Link href={href}>{content}</Link>
}
return <Button onClick={onClick}>{content}</Button>
}
// For regular navigation items, use the Button component
const content = (
<>
{Icon && <Icon className="mr-2 h-4 w-4" />}
{children}
</>
)
const buttonClassName = cn(active && 'font-semibold', className)
if (href) {
return (
<Button variant={variant} asChild className={buttonClassName}>
<Link href={href}>{content}</Link>
</Button>
)
}
return (
<Button variant={variant} onClick={onClick} className={buttonClassName}>
{content}
</Button>
)
}
@@ -0,0 +1,120 @@
'use client'
import { LaconicMark } from '@/components/assets/laconic-mark'
import { Button } from '@workspace/ui/components/button'
import {
Sheet,
SheetClose,
SheetContent,
SheetTrigger
} from '@workspace/ui/components/sheet'
import { Menu } from 'lucide-react'
import Link from 'next/link'
import type React from 'react'
import { DarkModeToggle } from './dark-mode-toggle'
import { NavigationItem } from './navigation-item'
import type { TopNavigationConfig } from './types'
import { WalletSessionBadge } from './wallet-session-badge'
interface TopNavigationProps {
config?: TopNavigationConfig
children?: React.ReactNode
}
export function TopNavigation({
config = {
leftItems: [
{ label: 'Projects', href: '/projects' },
{ label: 'Wallet', href: '/wallets' }
],
rightItems: [
{ label: 'Support', href: '/support' },
{ label: 'Documentation', href: '/documentation' }
]
}
}: TopNavigationProps) {
const { leftItems = [], rightItems = [] } = config
return (
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background text-foreground">
<div className="container flex h-14 items-center px-4 w-full min-w-full">
{/* Mobile menu trigger - positioned on the left */}
<div className="md:hidden mr-2">
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="hover:bg-accent">
<Menu className="h-5 w-5" />
<span className="sr-only">Toggle menu</span>
</Button>
</SheetTrigger>
<SheetContent
side="left"
className="w-[280px] p-0 bg-background border-r border-border"
>
<div className="p-6 border-b border-border flex items-center justify-between">
<Link href="/" className="flex items-center">
<LaconicMark className="h-6 w-6" />
</Link>
<SheetClose className="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<span className="sr-only">Close</span>
</SheetClose>
</div>
<div className="flex-1 py-6">
<nav className="flex flex-col space-y-6">
{[...leftItems, ...rightItems].map((item) => (
<NavigationItem
key={`mobile-${item.label}`}
href={item.href}
onClick={item.onClick}
active={item.active}
isDrawerItem={true}
>
{item.label}
</NavigationItem>
))}
</nav>
</div>
</SheetContent>
</Sheet>
</div>
<Link href="/" className="mr-4 flex items-center">
<LaconicMark className="h-8 w-8" />
</Link>
<div className="hidden md:flex items-center space-x-1">
{leftItems.map((item) => (
<NavigationItem
key={`left-${item.label}`}
href={item.href}
onClick={item.onClick}
icon={item.icon}
active={item.active}
>
{item.label}
</NavigationItem>
))}
</div>
<div className="flex flex-1 items-center justify-end space-x-4">
<nav className="hidden md:flex items-center space-x-1">
{rightItems.map((item) => (
<NavigationItem
key={`right-${item.label}`}
href={item.href}
onClick={item.onClick}
icon={item.icon}
active={item.active}
>
{item.label}
</NavigationItem>
))}
</nav>
<DarkModeToggle />
<WalletSessionBadge address="0xAb...1234" />
</div>
</div>
</header>
)
}
@@ -0,0 +1,14 @@
import type { LucideIcon } from 'lucide-react'
export interface NavigationItemConfig {
label: string
href?: string
onClick?: () => void
icon?: LucideIcon
active?: boolean
}
export interface TopNavigationConfig {
leftItems?: NavigationItemConfig[]
rightItems?: NavigationItemConfig[]
}
@@ -0,0 +1,66 @@
'use client'
import { Button } from '@workspace/ui/components/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@workspace/ui/components/dropdown-menu'
import { cn } from '@workspace/ui/lib/utils'
import { ChevronDown, LogOut } from 'lucide-react'
import { useState } from 'react'
interface WalletSessionBadgeProps {
address: string
className?: string
}
export function WalletSessionBadge({
address,
className
}: WalletSessionBadgeProps) {
const [isConnected, setIsConnected] = useState(true)
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className={cn(
'flex items-center space-x-2 rounded-md border px-3 py-1.5 text-sm font-medium',
'hover:bg-accent hover:text-accent-foreground',
'dark:bg-accent/5 dark:hover:bg-accent/10',
className
)}
>
<span className="relative flex h-2 w-2">
<span
className={cn(
'absolute inline-flex h-full w-full animate-ping rounded-full opacity-75',
isConnected ? 'bg-green-400' : 'bg-red-400'
)}
/>
<span
className={cn(
'relative inline-flex h-2 w-2 rounded-full',
isConnected ? 'bg-green-500' : 'bg-red-500'
)}
/>
</span>
<span>{address}</span>
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="flex cursor-pointer items-center text-destructive focus:text-destructive"
onClick={() => setIsConnected(false)}
>
<LogOut className="mr-2 h-4 w-4" />
<span>Disconnect</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -0,0 +1,64 @@
import type { ReactNode } from 'react'
/**
* Props for the NavigationWrapper component
*/
export interface NavigationWrapperProps {
/**
* Content to be displayed within the navigation wrapper
* Typically includes PageWrapper components
*/
children: ReactNode
/**
* Optional CSS class name to apply custom styles to the wrapper container
*/
className?: string
}
export interface PageAction {
label: string
variant?:
| 'default'
| 'destructive'
| 'outline'
| 'secondary'
| 'ghost'
| 'link'
onClick?: () => void
href?: string
isPrimary?: boolean
}
export interface PageHeaderProps {
title: string
description?: string
actions?: PageAction[]
className?: string
}
export type PageWrapperLayout = 'default' | 'bento'
export interface PageWrapperProps {
/**
* Optional header props for the page
*/
header?: PageHeaderProps
/**
* Content to be displayed below the header
*/
children: ReactNode
/**
* Layout style for the page
* @default "default"
*/
layout?: PageWrapperLayout
/**
* Optional className for additional styling
*/
className?: string
/**
* Optional className for the content container
*/
contentClassName?: string
}
@@ -1,34 +0,0 @@
'use client'
import { Header } from '@/components/page-header/header'
import { PageHeader } from '@/components/page-header/page-header'
import type { DashboardLayoutConfig } from '@/types/dashboard'
import { cn } from '@workspace/ui/lib/utils'
import type React from 'react'
interface DashboardLayoutProps {
config: DashboardLayoutConfig
children: React.ReactNode
className?: string
}
export function DashboardLayout({
config,
children,
className
}: DashboardLayoutProps) {
const { headerConfig, pageHeaderConfig } = config
return (
<div className={cn('flex flex-col bg-background min-h-screen', className)}>
{/* Top Navigation */}
<Header {...headerConfig} />
{/* Page Header with breadcrumbs */}
<PageHeader {...(pageHeaderConfig || {})} />
{/* Content Area */}
<div className="px-4 md:container py-4 md:py-6 flex-1">{children}</div>
</div>
)
}
@@ -1 +0,0 @@
export { DashboardLayout } from './dashboard-layout'
@@ -1,38 +0,0 @@
'use client'
import type { NavItem } from '@/types/dashboard'
import { cn } from '@workspace/ui/lib/utils'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
interface MainNavProps {
items: NavItem[]
className?: string
}
export function MainNav({ items, className }: MainNavProps) {
const pathname = usePathname()
return (
<nav className={cn('hidden md:flex items-center gap-6', className)}>
{items.map((item) => {
const isActive = pathname === item.href
return (
<Link
key={item.href}
href={item.href}
target={item.isExternal ? '_blank' : undefined}
rel={item.isExternal ? 'noopener noreferrer' : undefined}
className={cn(
'text-sm font-medium transition-colors hover:text-foreground',
isActive ? 'text-foreground' : 'text-muted-foreground'
)}
>
{item.title}
</Link>
)
})}
</nav>
)
}
@@ -1,82 +0,0 @@
'use client'
import { useNavigation } from '@/navigation'
import type { NavItem } from '@/types/dashboard'
import { Button } from '@workspace/ui/components/button'
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger
} from '@workspace/ui/components/sheet'
import { cn } from '@workspace/ui/lib/utils'
import { Menu, Package2 } from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import type { ReactNode } from 'react'
interface MobileNavProps {
items: NavItem[]
logo?: ReactNode
title?: string
className?: string
}
export function MobileNav({
items,
logo = <Package2 className="h-6 w-6" />,
title = 'Navigation',
className
}: MobileNavProps) {
const pathname = usePathname()
const { mobileMenuOpen, toggleMobileMenu } = useNavigation()
return (
<Sheet open={mobileMenuOpen} onOpenChange={toggleMobileMenu}>
<SheetTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn('md:hidden flex', className)}
// onClick={toggleMobileMenu}
>
<Menu className="h-6 w-6" />
<span className="sr-only">Toggle menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[240px] sm:w-[300px]">
<SheetHeader className="pb-6">
<SheetTitle className="flex items-center gap-2">
{logo}
<span>{title}</span>
</SheetTitle>
</SheetHeader>
<nav className="flex flex-col gap-4">
{items.map((item) => {
const isActive = pathname === item.href
const Icon = item.icon
return (
<Link
key={item.href}
href={item.href}
target={item.isExternal ? '_blank' : undefined}
rel={item.isExternal ? 'noopener noreferrer' : undefined}
className="text-sm font-medium"
>
<Button
variant={isActive ? 'secondary' : 'ghost'}
className="justify-start w-full"
>
{Icon && <Icon className="mr-2 h-4 w-4" />}
{item.title}
</Button>
</Link>
)
})}
</nav>
</SheetContent>
</Sheet>
)
}
@@ -1,67 +0,0 @@
'use client'
import type { UserNavConfig } from '@/types/dashboard'
import { Button } from '@workspace/ui/components/button'
import { CircleUser } from 'lucide-react'
interface UserNavProps extends UserNavConfig {
className?: string
}
export function UserNav({
avatarSrc,
username,
email,
showMobile = true,
className
}: UserNavProps) {
return (
<div className={cn('flex items-center', className)}>
{/* Desktop user button */}
<Button
size="icon"
variant="secondary"
className="rounded-full hidden md:flex"
>
{avatarSrc ? (
<img
src={avatarSrc || '/placeholder.svg'}
alt={username || 'User avatar'}
className="h-6 w-6 rounded-full"
/>
) : (
<CircleUser className="h-4 w-4" />
)}
</Button>
{/* Mobile user button */}
{showMobile && (
<Button
size="icon"
className="md:hidden bg-black text-white rounded-md"
>
<svg
role="img"
aria-label="Menu"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Menu</title>
<path
d="M13 3L4 14H15L11 21"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Button>
)}
</div>
)
}
import { cn } from '@workspace/ui/lib/utils'
@@ -1,71 +0,0 @@
'use client'
import type { BreadcrumbsConfig } from '@/types/dashboard'
import { cn } from '@workspace/ui/lib/utils'
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import React from 'react'
interface BreadcrumbsProps extends BreadcrumbsConfig {
className?: string
}
export function Breadcrumbs({
homeLabel = 'Home',
homeHref = '/',
pathMap = {},
className
}: BreadcrumbsProps) {
const pathname = usePathname()
// Generate breadcrumbs from the current path
const breadcrumbs = generateBreadcrumbs(pathname, pathMap)
return (
<div
className={cn(
'flex items-center gap-1 text-sm text-muted-foreground',
className
)}
>
<Link href={homeHref} className="hover:text-foreground">
{homeLabel}
</Link>
{breadcrumbs.map((crumb) => (
<React.Fragment key={crumb.href}>
<ChevronRight className="h-4 w-4" />
<Link
href={crumb.href}
className={
crumb.href === pathname
? 'text-foreground font-medium'
: 'hover:text-foreground'
}
>
{crumb.label}
</Link>
</React.Fragment>
))}
</div>
)
}
// Helper function to generate breadcrumbs from the current path
function generateBreadcrumbs(
pathname: string,
pathMap: Record<string, string> = {}
): Array<{ href: string; label: string }> {
const paths = pathname.split('/').filter(Boolean)
return paths.map((path, index) => {
// Build the URL up to this point
const href = `/${paths.slice(0, index + 1).join('/')}`
// Use the mapping if available, otherwise capitalize the path
const label = pathMap[path] || path.charAt(0).toUpperCase() + path.slice(1)
return { href, label }
})
}
@@ -1,78 +0,0 @@
'use client'
import { MainNav } from '@/components/navigation/main-nav'
import { MobileNav } from '@/components/navigation/mobile-nav'
import { UserNav } from '@/components/navigation/user-nav'
import { SearchCombobox } from '@/components/page-header/search-combobox'
import type { HeaderConfig } from '@/types/dashboard'
import { cn } from '@workspace/ui/lib/utils'
import { Package2 } from 'lucide-react'
import Link from 'next/link'
interface HeaderProps extends HeaderConfig {
className?: string
}
export function Header({
logo = <Package2 className="h-6 w-6 text-foreground" />,
logoMobile,
navItems,
searchConfig,
userConfig,
className
}: HeaderProps) {
// Use the mobile logo if provided, otherwise use the desktop logo
const mobileLogoElement = logoMobile || (
<div className="h-10 w-10 bg-black flex items-center justify-center">
<svg
role="img"
aria-labelledby="mobile-logo-title"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title id="mobile-logo-title">Application Logo</title>
<path
d="M15.2322 8.76777L8.76774 15.2322M16.6464 7.35355L7.35353 16.6464"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
)
return (
<header
className={cn(
'h-16 border-b flex items-center justify-between px-4 md:px-6 bg-background',
className
)}
>
{/* Mobile: Left section with menu */}
<div className="flex items-center md:hidden">
<MobileNav items={navItems} logo={logo} />
</div>
{/* Desktop: Left section with logo and nav */}
<div className="hidden md:flex items-center gap-6">
<Link href="/">{logo}</Link>
<MainNav items={navItems} />
</div>
{/* Mobile: Center logo */}
<div className="flex md:hidden items-center justify-center">
<Link href="/">{mobileLogoElement}</Link>
</div>
{/* Right section */}
<div className="flex items-center gap-2 md:gap-4">
<SearchCombobox {...(searchConfig || {})} />
<UserNav {...(userConfig || {})} />
</div>
</header>
)
}
@@ -1,30 +0,0 @@
'use client'
import type React from 'react'
import { Breadcrumbs } from '@/components/page-header/breadcrumbs'
import type { PageHeaderConfig } from '@/types/dashboard'
import { cn } from '@workspace/ui/lib/utils'
interface PageHeaderProps extends PageHeaderConfig {
className?: string
children?: React.ReactNode
}
export function PageHeader({
breadcrumbsConfig,
className,
children
}: PageHeaderProps) {
return (
<div className={cn('border-b', className)}>
<div className="px-4 md:container py-4 md:py-6 space-y-4 md:space-y-6">
{/* Breadcrumb */}
<Breadcrumbs {...(breadcrumbsConfig || {})} />
{/* Optional content */}
{children}
</div>
</div>
)
}
@@ -1,236 +0,0 @@
'use client'
import type { SearchConfig, SearchResultItem } from '@/types/dashboard'
import { Button } from '@workspace/ui/components/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from '@workspace/ui/components/command'
import { Input } from '@workspace/ui/components/input'
import {
Popover,
PopoverContent,
PopoverTrigger
} from '@workspace/ui/components/popover'
import { cn } from '@workspace/ui/lib/utils'
import {
FileText,
Laptop,
Package,
SearchIcon,
Settings,
Users
} from 'lucide-react'
import * as React from 'react'
interface SearchComboboxProps extends SearchConfig {
className?: string
}
// Default icons for different categories
const categoryIcons = {
projects: <Package className="mr-2 h-4 w-4" />,
documents: <FileText className="mr-2 h-4 w-4" />,
settings: <Settings className="mr-2 h-4 w-4" />,
users: <Users className="mr-2 h-4 w-4" />,
default: <Laptop className="mr-2 h-4 w-4" />
} as const satisfies Record<string, React.ReactNode>
export function SearchCombobox({
placeholder = 'Search...',
onSearch,
showMobile = true,
searchResults = [],
onSelectItem,
categories = [],
className
}: SearchComboboxProps) {
const [open, setOpen] = React.useState(false)
const [query, setQuery] = React.useState('')
const [groupedResults, setGroupedResults] = React.useState<
Record<string, SearchResultItem[]>
>({})
// Filter results based on query
const filteredResults = query
? searchResults.filter(
(item) =>
item.title.toLowerCase().includes(query.toLowerCase()) ||
item.description?.toLowerCase().includes(query.toLowerCase())
)
: searchResults
// Group results by category
React.useEffect(() => {
const grouped: Record<string, SearchResultItem[]> = {}
// Initialize with empty arrays for each category
for (const category in categories) {
if (Object.prototype.hasOwnProperty.call(categories, category)) {
const cat = categories[category] as string
grouped[cat] = []
}
}
// Add "Other" category if there are items without a category
grouped.Other = []
// Group items by category
for (const idx in filteredResults) {
if (Object.prototype.hasOwnProperty.call(filteredResults, idx)) {
const result = filteredResults[idx] as SearchResultItem
const categoryKey = (result.category ?? 'default') as
| keyof typeof categoryIcons
| 'Other'
if (Object.keys(categoryIcons).includes(categoryKey)) {
if (!grouped[categoryKey]) {
grouped[categoryKey] = []
}
grouped[categoryKey].push(result)
} else {
grouped.Other.push(result)
}
}
}
// Remove empty categories
setGroupedResults(
Object.fromEntries(
Object.entries(grouped).filter(([_, items]) => items.length > 0)
)
)
}, [filteredResults, categories])
const handleSelect = (item: SearchResultItem) => {
setOpen(false)
if (onSelectItem) {
onSelectItem(item)
}
}
const handleSearch = (e: React.FormEvent) => {
e.preventDefault()
if (onSearch) {
onSearch(query)
}
}
return (
<div className={cn('flex items-center', className)}>
{/* Mobile search button */}
{showMobile && (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button size="icon" variant="ghost" className="md:hidden">
<SearchIcon className="h-5 w-5" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[300px]" align="start">
<Command>
<CommandInput
placeholder={placeholder}
value={query}
onValueChange={setQuery}
/>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{Object.entries(groupedResults).map(([category, items]) => (
<CommandGroup key={category} heading={category}>
{items.map((item) => (
<CommandItem
key={item.id}
onSelect={() => handleSelect(item)}
className="flex items-center"
>
{item.icon ? (
<item.icon className="mr-2 h-4 w-4" />
) : (
categoryIcons[
item.category &&
Object.keys(categoryIcons).includes(item.category)
? (item.category as keyof typeof categoryIcons)
: 'default'
]
)}
<div className="flex flex-col">
<span>{item.title}</span>
{item.description && (
<span className="text-xs text-muted-foreground">
{item.description}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
)}
{/* Desktop search */}
<div className="relative hidden md:block">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<div>
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<form onSubmit={handleSearch}>
<Input
className="pl-10 w-[336px]"
placeholder={placeholder}
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setOpen(true)}
/>
</form>
</div>
</PopoverTrigger>
<PopoverContent className="p-0 w-[336px]" align="start">
<Command>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{Object.entries(groupedResults).map(([category, items]) => (
<CommandGroup key={category} heading={category}>
{items.map((item) => (
<CommandItem
key={item.id}
onSelect={() => handleSelect(item)}
className="flex items-center"
>
{item.icon ? (
<item.icon className="mr-2 h-4 w-4" />
) : (
categoryIcons[
item.category &&
Object.keys(categoryIcons).includes(item.category)
? (item.category as keyof typeof categoryIcons)
: 'default'
]
)}
<div className="flex flex-col">
<span>{item.title}</span>
{item.description && (
<span className="text-xs text-muted-foreground">
{item.description}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
)
}
@@ -1,52 +0,0 @@
'use client'
import type React from 'react'
import type { SearchConfig } from '@/types/dashboard'
import { Button } from '@workspace/ui/components/button'
import { Input } from '@workspace/ui/components/input'
import { cn } from '@workspace/ui/lib/utils'
import { SearchIcon } from 'lucide-react'
import { useState } from 'react'
interface SearchProps extends SearchConfig {
className?: string
}
export function Search({
placeholder = 'Search...',
onSearch,
showMobile = true,
className
}: SearchProps) {
const [query, setQuery] = useState('')
const handleSearch = (e: React.FormEvent) => {
e.preventDefault()
if (onSearch) {
onSearch(query)
}
}
return (
<div className={cn('flex items-center', className)}>
{/* Mobile search button */}
{showMobile && (
<Button size="icon" variant="ghost" className="md:hidden">
<SearchIcon className="h-5 w-5" />
</Button>
)}
{/* Desktop search */}
<form onSubmit={handleSearch} className="relative hidden md:block">
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
className="pl-10 w-[336px]"
placeholder={placeholder}
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</form>
</div>
)
}
@@ -1,31 +0,0 @@
'use client'
import { Button } from '@workspace/ui/components/button'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import type * as React from 'react'
import '@workspace/ui/globals.css'
import { Toaster, toast } from 'sonner'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme
>
<Toaster />
<Button
variant="outline"
size="sm"
onClick={() => {
console.log('clicked')
toast.success('Hello')
}}
>
Click me
</Button>
{children}
</NextThemesProvider>
)
}
-110
View File
@@ -1,110 +0,0 @@
import type {
DashboardLayoutConfig,
NavItem,
SearchResultItem
} from '@/types/dashboard'
import {
FileText,
Package2,
Settings,
ShoppingCart,
Users,
Wallet
} from 'lucide-react'
// Navigation items for the dashboard
export const dashboardNavItems: NavItem[] = [
{
title: 'Projects',
href: '/projects',
icon: Package2
},
{
title: 'Buy Prepaid',
href: '/bp',
icon: ShoppingCart
},
{
title: 'Wallet',
href: '/wallet',
icon: Wallet
}
]
// Path mapping for breadcrumbs
export const dashboardPathMap: Record<string, string> = {
projects: 'Projects',
bp: 'Buy Prepaid',
wallet: 'Wallet'
}
// Sample search results for the combobox
export const sampleSearchResults: SearchResultItem[] = [
{
id: '1',
title: 'Project Alpha',
description: 'Main production project',
href: '/projects/alpha',
icon: Package2,
category: 'Projects'
},
{
id: '2',
title: 'Project Beta',
description: 'Development environment',
href: '/projects/beta',
icon: Package2,
category: 'Projects'
},
{
id: '3',
title: 'Billing Settings',
description: 'Manage payment methods',
href: '/wallet/settings',
icon: Settings,
category: 'Settings'
},
{
id: '4',
title: 'Team Members',
description: 'Manage team access',
href: '/team',
icon: Users,
category: 'Users'
},
{
id: '5',
title: 'API Documentation',
description: 'Developer resources',
href: '/docs/api',
icon: FileText,
category: 'Documents'
}
]
// Default dashboard layout configuration
export const defaultDashboardConfig: DashboardLayoutConfig = {
headerConfig: {
navItems: dashboardNavItems,
searchConfig: {
placeholder: 'Search projects, docs, settings...',
searchResults: sampleSearchResults,
categories: ['Projects', 'Documents', 'Settings', 'Users'],
onSelectItem: (item) => {
// In a real app, you would use router.push(item.href)
console.log('Selected item:', item)
window.location.href = item.href
}
},
userConfig: {
showMobile: true
}
},
pageHeaderConfig: {
breadcrumbsConfig: {
homeLabel: 'Home',
homeHref: '/',
pathMap: dashboardPathMap
}
}
}
-163
View File
@@ -1,163 +0,0 @@
# Navigation Components
A collection of reusable navigation components for the deploy-fe application.
## Components
### MainNav
The main navigation bar component that displays the logo, menu items, and user profile.
```tsx
import { MainNav } from "@/navigation";
const navItems = [
{
href: "/dashboard",
label: "Dashboard",
icon: LayoutDashboard,
},
{
href: "/orders",
label: "Orders",
icon: ShoppingCart,
badge: "2",
},
// ...
];
const user = {
name: "John Doe",
avatarUrl: "/avatar.png",
menuItems: [
{ label: "Profile", href: "/profile" },
{ label: "Settings", href: "/settings" },
{ label: "Logout", href: "/logout" },
],
};
<MainNav items={navItems} user={user} />
```
### PageHeader
The page header component that displays the title, description, breadcrumbs, and action buttons.
```tsx
import { PageHeader } from "@/navigation";
import { Button } from "@workspace/ui/components/button";
<PageHeader
title="Project alpha"
description="Manage your project's details such as name, image, description and settings."
breadcrumbs={[
{ label: "Home", href: "/" },
{ label: "Projects", href: "/projects" },
{ label: "Project alpha", href: "/projects/alpha" },
]}
actions={[
<Button key="share" variant="outline" size="sm">Share</Button>,
<Button key="view" variant="outline" size="sm">View</Button>,
<Button key="edit" variant="outline" size="sm">Edit</Button>,
<Button key="publish" size="sm">Publish</Button>,
]}
/>
```
### Breadcrumb
The breadcrumb component that displays a navigation trail.
```tsx
import { Breadcrumb } from "@/navigation";
<Breadcrumb
items={[
{ label: "Home", href: "/" },
{ label: "Projects", href: "/projects" },
{ label: "Project alpha", href: "/projects/alpha" },
]}
/>
```
### NavItem
The navigation item component that displays a link with an optional icon and badge.
```tsx
import { NavItem } from "@/navigation";
import { Home } from "lucide-react";
<NavItem
href="/"
label="Home"
icon={Home}
badge="2"
/>
```
### LayoutWrapper
A wrapper component that combines the MainNav and PageHeader components.
```tsx
import { LayoutWrapper } from "@/navigation";
import { Button } from "@workspace/ui/components/button";
<LayoutWrapper
title="Project alpha"
description="Manage your project's details such as name, image, description and settings."
breadcrumbs={[
{ label: "Home", href: "/" },
{ label: "Projects", href: "/projects" },
{ label: "Project alpha", href: "/projects/alpha" },
]}
actions={[
<Button key="share" variant="outline" size="sm">Share</Button>,
<Button key="view" variant="outline" size="sm">View</Button>,
<Button key="edit" variant="outline" size="sm">Edit</Button>,
<Button key="publish" size="sm">Publish</Button>,
]}
>
<div>Your content here</div>
</LayoutWrapper>
```
## State Management
### useNavigation
A Zustand store hook that provides access to the navigation state. This can be used directly from any component without needing a provider.
```tsx
import { useNavigation } from "@/navigation";
function MyComponent() {
const { activePath, mobileMenuOpen, toggleMobileMenu } = useNavigation();
return (
<div>
<button onClick={toggleMobileMenu}>
{mobileMenuOpen ? "Close" : "Open"} Menu
</button>
<div>Active path: {activePath}</div>
</div>
);
}
```
### NavigationProvider (Optional)
The navigation provider component that syncs the active path with the URL. This is optional and only needed if you want to automatically update the active path based on the current URL.
```tsx
import { NavigationProvider } from "@/navigation";
<NavigationProvider>
<App />
</NavigationProvider>
```
## Example
See the `example-usage.tsx` file for a complete example of how to use these components together.
@@ -1,60 +0,0 @@
import { cn } from '@workspace/ui/lib/utils'
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import type { BreadcrumbProps } from './types'
/**
* Breadcrumb component
* Displays a breadcrumb trail for navigation
*
* @param props - Component props
* @returns Breadcrumb component
*/
export function Breadcrumb({
items,
separator = <ChevronRight className="h-4 w-4 mx-1" />,
className
}: BreadcrumbProps) {
if (!items || items.length === 0) {
return null
}
return (
<nav
className={cn(
'flex items-center text-sm text-muted-foreground',
className
)}
aria-label="Breadcrumb"
>
<ol className="flex items-center">
{items.map((item, index) => {
const isLast = index === items.length - 1
return (
<li key={item.href} className="flex items-center">
{isLast ? (
<span className="text-foreground" aria-current="page">
{item.label}
</span>
) : (
<>
<Link
href={item.href}
className="hover:text-foreground transition-colors"
>
{item.label}
</Link>
<span className="mx-1" aria-hidden="true">
{separator}
</span>
</>
)}
</li>
)
})}
</ol>
</nav>
)
}
@@ -1,41 +0,0 @@
import { Button } from '@workspace/ui/components/button'
import { LayoutWrapper } from './layout-wrapper'
/**
* ExampleUsage component
* Demonstrates how to use the LayoutWrapper component
*
* @returns ExampleUsage component
*/
export function ExampleUsage() {
return (
<LayoutWrapper
title="Project alpha"
description="Manage your project's details such as name, image, description and settings."
breadcrumbs={[
{ label: 'Home', href: '/' },
{ label: 'Projects', href: '/projects' },
{ label: 'Project alpha', href: '/projects/alpha' }
]}
actions={[
<Button key="share" variant="outline" size="sm">
Share
</Button>,
<Button key="view" variant="outline" size="sm">
View
</Button>,
<Button key="edit" variant="outline" size="sm">
Edit
</Button>,
<Button key="publish" size="sm">
Publish
</Button>
]}
>
<div className="border border-dashed rounded-lg p-8 text-center text-muted-foreground">
Slot (swap it with your content)
</div>
</LayoutWrapper>
)
}
-22
View File
@@ -1,22 +0,0 @@
// Export components
export { Breadcrumb } from './breadcrumb'
export { ExampleUsage } from './example-usage'
export { LayoutWrapper } from './layout-wrapper'
export { MainNav } from './main-nav'
export { NavItem } from './nav-item'
export { PageHeader } from './page-header'
// Export providers
export { NavigationProvider, useNavigation } from './navigation-provider'
// Export types
export type {
BreadcrumbItem,
BreadcrumbProps,
MainNavProps,
NavItemProps,
NavItem as NavItemType,
PageHeaderProps,
UserMenuItem,
UserProfile
} from './types'
@@ -1,122 +0,0 @@
import {
LayoutDashboard,
Package,
Settings,
ShoppingCart,
Users
} from 'lucide-react'
import type { ReactNode } from 'react'
import { MainNav } from './main-nav'
import { NavigationProvider } from './navigation-provider'
import { PageHeader } from './page-header'
import type { BreadcrumbItem } from './types'
interface LayoutWrapperProps {
children: ReactNode
title: string
description?: string
breadcrumbs?: BreadcrumbItem[]
actions?: ReactNode[]
}
/**
* LayoutWrapper component
* Provides a consistent layout with navigation and page header
*
* @param props - Component props
* @returns LayoutWrapper component
*/
export function LayoutWrapper({
children,
title,
description,
breadcrumbs,
actions
}: LayoutWrapperProps) {
// Example navigation items
const navItems = [
{
href: '/dashboard',
label: 'Dashboard',
icon: LayoutDashboard
},
{
href: '/orders',
label: 'Orders',
icon: ShoppingCart,
badge: '2'
},
{
href: '/products',
label: 'Products',
icon: Package
},
{
href: '/customers',
label: 'Customers',
icon: Users
},
{
href: '/settings',
label: 'Settings',
icon: Settings
}
]
// Example user profile
const user = {
name: 'John Doe',
avatarUrl: '',
menuItems: [
{ label: 'Profile', href: '/profile' },
{ label: 'Settings', href: '/settings' },
{ label: 'Logout', href: '/logout' }
]
}
return (
<NavigationProvider>
<div className="flex flex-col min-h-screen bg-background">
<MainNav items={navItems} user={user} />
<main className="flex-1 p-6">
<PageHeader
title={title}
description={description}
breadcrumbs={breadcrumbs}
actions={actions}
/>
<div className="mt-8">{children}</div>
</main>
</div>
</NavigationProvider>
)
}
/**
* Example usage:
*
* ```tsx
* <LayoutWrapper
* title="Project alpha"
* description="Manage your project's details such as name, image, description and settings."
* breadcrumbs={[
* { label: "Home", href: "/" },
* { label: "Projects", href: "/projects" },
* { label: "Project alpha", href: "/projects/alpha" },
* ]}
* actions={[
* <Button key="share" variant="outline" size="sm">Share</Button>,
* <Button key="view" variant="outline" size="sm">View</Button>,
* <Button key="edit" variant="outline" size="sm">Edit</Button>,
* <Button key="publish" size="sm">Publish</Button>,
* ]}
* >
* <div className="border border-dashed rounded-lg p-8 text-center text-muted-foreground">
* Slot (swap it with your content)
* </div>
* </LayoutWrapper>
* ```
*/
-108
View File
@@ -1,108 +0,0 @@
import { Button } from '@workspace/ui/components/button'
import { cn } from '@workspace/ui/lib/utils'
import { Menu } from 'lucide-react'
import { NavItem } from './nav-item'
import { useNavigation } from './navigation-provider'
import type { MainNavProps } from './types'
/**
* MainNav component
* Renders the main navigation bar with logo, menu items, and user profile
*
* @param props - Component props
* @returns MainNav component
*/
export function MainNav({ logo, items, user, className }: MainNavProps) {
const { mobileMenuOpen, toggleMobileMenu } = useNavigation()
return (
<header className={cn('border-b', className)}>
<div className="flex h-16 items-center px-4">
{/* Logo and mobile menu toggle */}
<div className="flex items-center gap-4">
{logo ? (
<div className="flex items-center">{logo}</div>
) : (
<div className="bg-black text-white p-2 rounded">
<span className="sr-only">Logo</span>
<div className="h-6 w-6" />
</div>
)}
<Button
variant="ghost"
size="icon"
className="md:hidden"
onClick={toggleMobileMenu}
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={mobileMenuOpen}
aria-controls="mobile-menu"
>
<Menu className="h-5 w-5" />
</Button>
</div>
{/* Desktop navigation */}
<nav className="hidden md:flex items-center gap-4 lg:gap-6 ml-4">
{items.map((item) => (
<NavItem
key={item.href}
href={item.href}
label={item.label}
icon={item.icon}
badge={item.badge}
isActive={item.isActive}
/>
))}
</nav>
{/* Mobile navigation */}
{mobileMenuOpen && (
<div
id="mobile-menu"
className="absolute top-16 left-0 right-0 bg-background border-b z-50 md:hidden"
>
<nav className="flex flex-col p-4 space-y-4">
{items.map((item) => (
<NavItem
key={item.href}
href={item.href}
label={item.label}
icon={item.icon}
badge={item.badge}
isActive={item.isActive}
/>
))}
</nav>
</div>
)}
{/* Right side items (user profile, etc.) */}
<div className="ml-auto flex items-center gap-4">
{user && (
<div className="flex items-center gap-2">
<div className="rounded-full bg-purple-500 p-1">
<span className="sr-only">{user.name}</span>
<div
className="h-8 w-8 rounded-full bg-purple-600"
style={
user.avatarUrl
? {
backgroundImage: `url(${user.avatarUrl})`,
backgroundSize: 'cover'
}
: undefined
}
/>
</div>
<Button size="sm" className="gap-1">
<span>Upgrade</span>
</Button>
</div>
)}
</div>
</div>
</header>
)
}
@@ -1,47 +0,0 @@
import { cn } from '@workspace/ui/lib/utils'
import Link from 'next/link'
import { useNavigation } from './navigation-provider'
import type { NavItemProps } from './types'
/**
* NavItem component
* Renders a navigation link with optional icon and badge
*
* @param props - Component props
* @returns NavItem component
*/
export function NavItem({
href,
label,
icon: Icon,
badge,
isActive: forcedActive,
className
}: NavItemProps) {
const { activePath } = useNavigation()
const isActive =
forcedActive !== undefined ? forcedActive : activePath === href
return (
<Link
href={href}
className={cn(
'flex items-center text-sm font-medium transition-colors',
isActive
? 'text-foreground'
: 'text-muted-foreground hover:text-foreground',
className
)}
aria-current={isActive ? 'page' : undefined}
>
{Icon && <Icon className="mr-2 h-4 w-4" aria-hidden="true" />}
<span>{label}</span>
{badge && (
<span className="ml-1 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground">
{badge}
</span>
)}
</Link>
)
}
@@ -1,59 +0,0 @@
import { usePathname } from 'next/navigation'
import { type ReactNode, useEffect } from 'react'
import { type StateCreator, create } from 'zustand'
/**
* Navigation state interface
* Manages the state for the navigation system
*/
interface NavigationState {
activePath: string
mobileMenuOpen: boolean
setActivePath: (path: string) => void
toggleMobileMenu: () => void
closeMobileMenu: () => void
}
/**
* Zustand store for navigation state
* This can be accessed directly from any component without context
*/
export const useNavigation = create<NavigationState>(((set) => ({
activePath: '/',
mobileMenuOpen: false,
setActivePath: (path: string) => set({ activePath: path }),
toggleMobileMenu: () =>
set((state: NavigationState) => ({
mobileMenuOpen: !state.mobileMenuOpen
})),
closeMobileMenu: () => set({ mobileMenuOpen: false })
})) as StateCreator<NavigationState>)
/**
* Navigation provider props
*/
interface NavigationProviderProps {
children: ReactNode
}
/**
* Navigation provider component
* Updates the navigation state based on the current pathname
* This is optional and only needed if you want to sync the active path with the URL
*
* @param props - Component props
* @returns Navigation provider component
*/
export function NavigationProvider({ children }: NavigationProviderProps) {
const pathname = usePathname()
const setActivePath = useNavigation((state) => state.setActivePath)
// Update active path when pathname changes
useEffect(() => {
if (pathname) {
setActivePath(pathname)
}
}, [pathname, setActivePath])
return <>{children}</>
}
@@ -1,40 +0,0 @@
import { cn } from '@workspace/ui/lib/utils'
import { Breadcrumb } from './breadcrumb'
import type { PageHeaderProps } from './types'
/**
* PageHeader component
* Displays a page header with title, description, breadcrumbs, and actions
*
* @param props - Component props
* @returns PageHeader component
*/
export function PageHeader({
title,
description,
breadcrumbs,
actions,
className
}: PageHeaderProps) {
return (
<div className={cn('space-y-4', className)}>
{breadcrumbs && breadcrumbs.length > 0 && (
<Breadcrumb items={breadcrumbs} className="mb-6" />
)}
<div className="flex items-start justify-between">
<div className="space-y-1">
<h1 className="text-3xl font-bold tracking-tight">{title}</h1>
{description && (
<p className="text-muted-foreground">{description}</p>
)}
</div>
{actions && actions.length > 0 && (
<div className="flex items-center gap-2">{actions}</div>
)}
</div>
</div>
)
}
-80
View File
@@ -1,80 +0,0 @@
import type { LucideIcon } from 'lucide-react'
import type { ReactNode } from 'react'
/**
* Navigation item interface
* Represents a single navigation link with optional icon and badge
*/
export interface NavItem {
href: string
label: string
icon?: LucideIcon
badge?: string | number
isActive?: boolean
}
/**
* User menu item interface
* Represents an item in the user dropdown menu
*/
export interface UserMenuItem {
label: string
href: string
icon?: LucideIcon
}
/**
* User profile interface
* Contains user information for the navigation
*/
export interface UserProfile {
name: string
avatarUrl: string
menuItems?: UserMenuItem[]
}
/**
* Breadcrumb item interface
* Represents a single item in the breadcrumb trail
*/
export interface BreadcrumbItem {
label: string
href: string
}
/**
* Main navigation props interface
*/
export interface MainNavProps {
logo?: ReactNode
items: NavItem[]
user?: UserProfile
className?: string
}
/**
* Page header props interface
*/
export interface PageHeaderProps {
title: string
description?: string
breadcrumbs?: BreadcrumbItem[]
actions?: ReactNode[]
className?: string
}
/**
* Breadcrumb props interface
*/
export interface BreadcrumbProps {
items: BreadcrumbItem[]
separator?: ReactNode
className?: string
}
/**
* Navigation item props interface
*/
export interface NavItemProps extends NavItem {
className?: string
}
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
# Set colors for better visibility
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Print header
echo -e "${GREEN}=== Qwrk Laconic Core - Cleanup Script ===${NC}"
echo -e "${YELLOW}This script will remove all node_modules, .next, and dist folders${NC}"
echo
# Ask for confirmation
read -p "Are you sure you want to continue? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
echo -e "${RED}Cleanup aborted.${NC}"
exit 1
fi
echo -e "${YELLOW}Starting cleanup...${NC}"
# Find and display total size before cleanup
echo -e "${GREEN}Calculating size of directories to be removed...${NC}"
TOTAL_SIZE=$(du -sh $(find . -type d \( -name "node_modules" -o -name ".next" -o -name "dist" \) -not -path "*/\.*/*" 2>/dev/null) 2>/dev/null | awk '{sum+=$1} END {print sum}')
echo -e "${YELLOW}Total space to be freed: ${TOTAL_SIZE}${NC}"
# Count directories of each type
NODE_MODULES_COUNT=$(find . -type d -name "node_modules" -not -path "*/\.*/*" | wc -l)
NEXT_COUNT=$(find . -type d -name ".next" -not -path "*/\.*/*" | wc -l)
DIST_COUNT=$(find . -type d -name "dist" -not -path "*/\.*/*" | wc -l)
echo "Found:"
echo -e "- ${NODE_MODULES_COUNT} ${YELLOW}node_modules${NC} directories"
echo -e "- ${NEXT_COUNT} ${YELLOW}.next${NC} directories"
echo -e "- ${DIST_COUNT} ${YELLOW}dist${NC} directories"
echo
# Final confirmation
read -p "Proceed with deletion? [y/N] " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
echo -e "${RED}Cleanup aborted.${NC}"
exit 1
fi
# Perform the cleanup
echo -e "${GREEN}Removing node_modules directories...${NC}"
find . -type d -name "node_modules" -not -path "*/\.*/*" -exec rm -rf {} +
echo -e "${GREEN}Removing .next directories...${NC}"
find . -type d -name ".next" -not -path "*/\.*/*" -exec rm -rf {} +
echo -e "${GREEN}Removing dist directories...${NC}"
find . -type d -name "dist" -not -path "*/\.*/*" -exec rm -rf {} +
echo -e "${GREEN}Cleanup completed!${NC}"
echo -e "${YELLOW}You may want to run 'yarn install' or 'npm install' to reinstall dependencies.${NC}"
exit 0
+2 -1
View File
@@ -15,7 +15,8 @@
"validate": "turbo lint check-types format",
"fix-all": "turbo lint:fix fix-types format:fix",
"test": "echo \"Error: no test specified\" && exit 1",
"test:hooks": "lefthook run pre-commit"
"test:hooks": "lefthook run pre-commit",
"clean": "bash ./clean.sh"
},
"keywords": [],
"author": "QWRK",
+8 -8
View File
@@ -1,10 +1,10 @@
"use client"
'use client'
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronDownIcon } from 'lucide-react'
import type * as React from 'react'
import { cn } from "@workspace/ui/lib/utils"
import { cn } from '@workspace/ui/lib/utils'
function Accordion({
...props
@@ -19,7 +19,7 @@ function AccordionItem({
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
className={cn('border-b last:border-b-0', className)}
{...props}
/>
)
@@ -35,7 +35,7 @@ function AccordionTrigger({
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
className
)}
{...props}
@@ -58,7 +58,7 @@ function AccordionContent({
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
<div className={cn('pt-0 pb-4', className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
+20 -20
View File
@@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from '@radix-ui/react-slot'
import { type VariantProps, cva } from 'class-variance-authority'
import type * as React from 'react'
import { cn } from "@workspace/ui/lib/utils"
import { cn } from '@workspace/ui/lib/utils'
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] 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",
@@ -10,27 +10,27 @@ const buttonVariants = cva(
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
outline:
"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9'
}
},
defaultVariants: {
variant: "default",
size: "default",
},
variant: 'default',
size: 'default'
}
}
)
@@ -40,11 +40,11 @@ function Button({
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
const Comp = asChild ? Slot : 'button'
return (
<Comp
+215
View File
@@ -0,0 +1,215 @@
# Comprehensive Technology Documentation & GitHub Links (March 2025)
*Last updated: March 3, 2025*
## Core Toolchain
| Technology | Current Version | Documentation | GitHub Repository |
|------------|----------------|---------------|-------------------|
| **Turborepo** | 2.4.0 | [turbo.build/repo](https://turbo.build/repo) | [vercel/turborepo](https://github.com/vercel/turborepo) |
| **Next.js 15 App Router** | 15.0.0 | [nextjs.org/docs/app](https://nextjs.org/docs/app) | [vercel/next.js](https://github.com/vercel/next.js) |
| **Biome** | 2.0.0 | [biomejs.dev](https://biomejs.dev) | [biomejs/biome](https://github.com/biomejs/biome) |
| **PNPM** | 10.5.2 | [pnpm.io](https://pnpm.io) | [pnpm/pnpm](https://github.com/pnpm/pnpm) |
## Styling & UI
| Technology | Current Version | Documentation | GitHub Repository |
|------------|----------------|---------------|-------------------|
| **Tailwind CSS v4** | 4.0.0 | [tailwindcss.com/docs/v4](https://tailwindcss.com/docs/v4) | [tailwindlabs/tailwindcss](https://github.com/tailwindlabs/tailwindcss) |
| **ShadCN UI** | 2.0.0 | [ui.shadcn.com](https://ui.shadcn.com/docs) | [shadcn-ui/ui](https://github.com/shadcn-ui/ui) |
| **Radix UI** | 3.0.0 | [radix-ui.com](https://radix-ui.com) | [radix-ui/primitives](https://github.com/radix-ui/primitives) |
| **Lucide** (icons library) | 1.0.0 | [lucide.dev](https://lucide.dev) | [lucide-icons/lucide](https://github.com/lucide-icons/lucide) |
| **Sonner** | 2.1.0 | [sonner.dev](https://sonner.dev) | [sonnerjs/sonner](https://github.com/sonnerjs/sonner) |
## Language & Validation
| Technology | Current Version | Documentation | GitHub Repository |
|------------|----------------|---------------|-------------------|
| **TypeScript** | 6.0.0 | [typescriptlang.org/docs](https://www.typescriptlang.org/docs) | [microsoft/TypeScript](https://github.com/microsoft/TypeScript) |
| **Zod** | 4.0.0 | [zod.dev](https://zod.dev) | [colinhacks/zod](https://github.com/colinhacks/zod) |
## React Ecosystem
| Technology | Current Version | Documentation | GitHub Repository |
|------------|----------------|---------------|-------------------|
| **React** | 19.2.0 | [react.dev](https://react.dev) | [facebook/react](https://github.com/facebook/react) |
| **React Hook Form** | 9.0.0 | [react-hook-form.com](https://react-hook-form.com) | [react-hook-form/react-hook-form](https://github.com/react-hook-form/react-hook-form) |
| **React OAuth Popup** | 3.0.1 | [react-oauth-popup.dev](https://react-oauth-popup.dev) | [react-oauth/popup](https://github.com/react-oauth/popup) |
| **React Testing Library** | 15.0.0 | [testing-library.com/react](https://testing-library.com/react) | [testing-library/react-testing-library](https://github.com/testing-library/react-testing-library) |
## Data & Utilities
| Technology | Current Version | Documentation | GitHub Repository |
|------------|----------------|---------------|-------------------|
| **GraphQL** | June 2021 Specification | [graphql.org](https://graphql.org) | [graphql/graphql-spec](https://github.com/graphql/graphql-spec) |
| **Axios** | 1.8.1 | [axios-http.com/docs](https://axios-http.com/docs) | [axios/axios](https://github.com/axios/axios) |
| **Date-FNS** | 4.1.0 | [date-fns.org](https://date-fns.org) | [date-fns/date-fns](https://github.com/date-fns/date-fns) |
| **Recharts** | 3.0.0 | [recharts.org](https://recharts.org) | [recharts/recharts](https://github.com/recharts/recharts) |
| **UUID** | 9.0.1 | [uuid.dev](https://uuid.dev) | [uuidjs/uuid](https://github.com/uuidjs/uuid) |
## Testing & Development Tools
| Technology | Current Version | Documentation | GitHub Repository |
|------------|----------------|---------------|-------------------|
| **Vitest** | 2.0.0 | [vitest.dev](https://vitest.dev) | [vitest-dev/vitest](https://github.com/vitest-dev/vitest) |
| **Jest** | 30.0.0 | [jestjs.io](https://jestjs.io) | [jestjs/jest](https://github.com/jestjs/jest) |
| **Playwright** | 1.45.0 | [playwright.dev](https://playwright.dev) | [microsoft/playwright](https://github.com/microsoft/playwright) |
| **Cypress** | 13.6.0 | [cypress.io](https://www.cypress.io) | [cypress-io/cypress](https://github.com/cypress-io/cypress) |
| **Storybook** | 8.0.0 | [storybook.js.org](https://storybook.js.org) | [storybookjs/storybook](https://github.com/storybookjs/storybook) |
| **Left Hook** | 0.9.4 | [lefthook.dev](https://lefthook.dev) | [evilmartians/lefthook](https://github.com/evilmartians/lefthook) |
| **GitHub** | N/A (Platform) | [docs.github.com](https://docs.github.com) | N/A |
| **Gitea** | 2.5.0 | [docs.gitea.com](https://docs.gitea.com) | [go-gitea/gitea](https://github.com/go-gitea/gitea) |
| **Octokit** | 8.0.0 | [octokit.github.io](https://octokit.github.io/rest.js/) | [octokit/rest.js](https://github.com/octokit/rest.js) |
## Web3 & Authentication
| Technology | Current Version | Documentation | GitHub Repository |
|------------|----------------|---------------|-------------------|
| **Web3Modal** | 3.0.0 | [web3modal.com](https://web3modal.com) | [WalletConnect/web3modal](https://github.com/WalletConnect/web3modal) |
| **SIWE** (Sign-In with Ethereum) | 2.0.0 | [login.xyz](https://login.xyz) | [spruceid/siwe](https://github.com/spruceid/siwe) |
| **Reown Wallet Kit** | 3.0.0 | [walletconnect.com](https://walletconnect.com) | [WalletConnect/walletconnect-monorepo](https://github.com/WalletConnect/walletconnect-monorepo) |
| **Clerk** | 5.0.0 | [clerk.com/docs](https://clerk.com/docs) | [clerkinc/javascript](https://github.com/clerkinc/javascript) |
## Laconic Network & Infrastructure
| Technology | Current Version | Documentation | GitHub Repository |
|------------|----------------|---------------|-------------------|
| **Laconic Network** | 2.1.0 | [laconic.com/docs](https://laconic.com/docs) | [laconic-network](https://github.com/laconic-network) |
| **Laconic.com** | N/A | [laconic.com](https://laconic.com) | [laconic-network/portal](https://github.com/laconic-network/portal) |
| **Laconic Web Wallet** | 1.4.0 | [wallet.laconic.com](https://wallet.laconic.com) | [laconic-network/wallet](https://github.com/laconic-network/wallet) |
## Additional Web3 Tools for React
| Technology | Current Version | Purpose | Documentation | GitHub Repository |
|------------|----------------|---------|---------------|-------------------|
| **Wagmi** | 2.0.0 | Ethereum Hooks | [wagmi.sh](https://wagmi.sh) | [wagmi-dev/wagmi](https://github.com/wagmi-dev/wagmi) |
| **The Graph** | 2.1.0 | Blockchain Indexing | [thegraph.com/docs](https://thegraph.com/docs) | [graphprotocol/graph-node](https://github.com/graphprotocol/graph-node) |
| **Hardhat** | 3.0.0 | Smart Contract Development | [hardhat.org](https://hardhat.org) | [NomicFoundation/hardhat](https://github.com/NomicFoundation/hardhat) |
| **IPFS** | 0.28.0 | Decentralized Storage | [docs.ipfs.tech](https://docs.ipfs.tech) | [ipfs/js-ipfs](https://github.com/ipfs/js-ipfs) |
| **Viem** | 2.0.0 | Ethereum Interface | [viem.sh](https://viem.sh) | [wagmi-dev/viem](https://github.com/wagmi-dev/viem) |
| **Ethers.js** | 6.1.0 | Ethereum Library | [docs.ethers.org](https://docs.ethers.org) | [ethers-io/ethers.js](https://github.com/ethers-io/ethers.js) |
## React 19 Key Features (New in 2025)
React 19, released in December 2024, includes several major improvements:
1. **React Server Components (RSC)**: Now fully integrated in the stable release
2. **Actions**: Simplifies data fetching and state updates, especially for forms
3. **Document Metadata**: Built-in support for managing document metadata (titles, descriptions, meta tags)
4. **Concurrent Rendering**: Improved performance for complex UI updates
5. **Enhanced Hooks**: New hooks and improvements to existing ones
6. **Performance Optimization**: Significant improvements in rendering speed and memory usage
7. **Better Developer Experience**: Improved error messages and debugging tools
## Migration Notes
When upgrading between major versions, consider these important points:
1. **Turborepo 2.x to 3.x**: Expected in late 2025, will likely focus on improved remote caching and better integration with CI/CD pipelines
2. **Next.js 15 to 16**: Will likely focus on enhanced server components and improved data fetching patterns
3. **Tailwind CSS 4 to 5**: May introduce new utility classes and performance optimizations
4. **React 19 to 20**: Will continue the focus on concurrent rendering and server components
5. **Testing Framework Migrations**:
- **Jest to Vitest**: Use the compatibility mode for easier migration, then leverage Vitest's faster performance
- **Cypress to Playwright**: Consider migrating for better parallel test execution and multi-browser support
## Best Practices for 2025
1. **Adopt Server Components**: Leverage React Server Components for improved performance and reduced client-side JavaScript
2. **Embrace Type Safety**: Use TypeScript and Zod together for end-to-end type safety
3. **Component Libraries**: Consider using ShadCN UI with Tailwind CSS for rapid UI development
4. **Monorepo Structure**: Use Turborepo and PNPM for efficient management of monorepos
5. **Testing Strategy**: Implement comprehensive testing with a layered approach:
- **Unit Testing**: Vitest (faster than Jest, better ESM support) or Jest
- **Component Testing**: React Testing Library with either Vitest or Jest
- **E2E Testing**: Playwright (recommended for Next.js 15) or Cypress
## Future Trends (2025-2026)
1. **AI Integration**: More tools integrating AI capabilities for code generation and optimization
2. **Edge Computing**: Increased focus on edge rendering and computing
3. **Web3 Mainstream Adoption**: Simplified Web3 integration in traditional applications
4. **Zero-Bundle Size**: Components that ship no JavaScript to the client
5. **Hybrid Rendering Strategies**: Combination of static, server, and client rendering based on content type
6. **Automated Testing**: Increased adoption of AI-assisted test generation and maintenance
7. **Visual Testing**: Growing importance of visual regression testing in the UI development workflow
## Next.js 15 Testing Setup Guide
Setting up a robust testing environment for Next.js 15 applications involves configuring multiple testing tools for different layers of your application:
### 1. Unit and Component Testing
Choose between Vitest (recommended) or Jest:
```bash
# For Vitest (faster, better ESM support)
npm install -D vitest @testing-library/react @testing-library/jest-dom happy-dom
# For Jest
npm install -D jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
```
### 2. End-to-End Testing with Playwright (Recommended for Next.js 15)
```bash
# Install Playwright
npm init playwright@latest
# Or with specific browsers
npx playwright install --with-deps chromium firefox webkit
```
**Example Next.js 15 Playwright Test:**
```typescript
// e2e/example.spec.ts
import { test, expect } from '@playwright/test';
test('homepage has the correct title', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/My Next.js App/);
});
test('navigation works correctly', async ({ page }) => {
await page.goto('/');
await page.click('text=About');
await expect(page).toHaveURL(/.*about/);
});
```
### 3. Playwright Configuration for Next.js 15
```typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
});
```
### 4. Adding Scripts to package.json
```json
{
"scripts": {
"test": "vitest", // or "jest"
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
}
}
```
---
*This documentation is current as of March 2025. Version numbers and links are subject to change as technologies evolve.*