create-kuckit-app 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/base/apps/web/src/components/KuckitModuleRoute.tsx +47 -10
- package/templates/base/apps/web/src/components/dashboard/app-sidebar.tsx +120 -0
- package/templates/base/apps/web/src/components/dashboard/dashboard-layout.tsx +46 -0
- package/templates/base/apps/web/src/components/dashboard/dashboard-overview.tsx +24 -0
- package/templates/base/apps/web/src/components/dashboard/index.ts +2 -0
- package/templates/base/apps/web/src/components/dashboard/nav-user.tsx +77 -0
- package/templates/base/apps/web/src/components/ui/avatar.tsx +39 -0
- package/templates/base/apps/web/src/components/ui/breadcrumb.tsx +102 -0
- package/templates/base/apps/web/src/components/ui/collapsible.tsx +21 -0
- package/templates/base/apps/web/src/components/ui/separator.tsx +26 -0
- package/templates/base/apps/web/src/components/ui/sheet.tsx +130 -0
- package/templates/base/apps/web/src/components/ui/sidebar.tsx +694 -0
- package/templates/base/apps/web/src/components/ui/skeleton.tsx +13 -0
- package/templates/base/apps/web/src/components/ui/tooltip.tsx +55 -0
- package/templates/base/apps/web/src/hooks/use-mobile.ts +19 -0
- package/templates/base/apps/web/src/lib/utils.ts +6 -0
- package/templates/base/apps/web/src/modules.client.ts +1 -1
- package/templates/base/apps/web/src/providers/KuckitProvider.tsx +1 -25
- package/templates/base/apps/web/src/routes/dashboard/$.tsx +9 -0
- package/templates/base/apps/web/src/routes/dashboard/index.tsx +6 -0
- package/templates/base/apps/web/src/routes/dashboard.tsx +25 -0
- package/templates/base/apps/web/src/lib/kuckit-router.ts +0 -42
package/package.json
CHANGED
|
@@ -3,13 +3,25 @@ import { useKuckit } from '@/providers/KuckitProvider'
|
|
|
3
3
|
import { useServices } from '@/providers/ServicesProvider'
|
|
4
4
|
import { useEffect, useState } from 'react'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Helper to determine if a route path is a root-level route (outside dashboard).
|
|
8
|
+
* Root routes start with '/' but NOT '/dashboard'.
|
|
9
|
+
*/
|
|
10
|
+
function isRootLevelRoute(path: string): boolean {
|
|
11
|
+
return path.startsWith('/') && !path.startsWith('/dashboard')
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
/**
|
|
7
15
|
* Dynamic route renderer for Kuckit module routes.
|
|
8
16
|
*
|
|
9
17
|
* This component looks up the current path in the RouteRegistry
|
|
10
18
|
* and renders the corresponding module component if found.
|
|
11
19
|
*
|
|
12
|
-
*
|
|
20
|
+
* Route resolution is context-aware:
|
|
21
|
+
* - When rendered under /dashboard/*: matches dashboard routes
|
|
22
|
+
* - When rendered at root level: matches root-level routes (paths starting with '/' but not '/dashboard/')
|
|
23
|
+
*
|
|
24
|
+
* For root routes with meta.requiresAuth: true, redirects to /login if not authenticated.
|
|
13
25
|
*/
|
|
14
26
|
export function KuckitModuleRoute() {
|
|
15
27
|
const { routeRegistry } = useKuckit()
|
|
@@ -21,10 +33,28 @@ export function KuckitModuleRoute() {
|
|
|
21
33
|
const [authChecked, setAuthChecked] = useState(false)
|
|
22
34
|
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
|
23
35
|
|
|
24
|
-
//
|
|
25
|
-
const
|
|
36
|
+
// Determine if we're in dashboard context
|
|
37
|
+
const isDashboardContext = pathname.startsWith('/dashboard')
|
|
38
|
+
|
|
39
|
+
// Filter routes based on context
|
|
40
|
+
const contextRoutes = routeRegistry.getAll().filter((r) => {
|
|
41
|
+
const isRoot = isRootLevelRoute(r.path)
|
|
42
|
+
// In dashboard context: show non-root routes
|
|
43
|
+
// In root context: show root routes
|
|
44
|
+
return isDashboardContext ? !isRoot : isRoot
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// For dashboard context, also try matching with /dashboard prefix stripped
|
|
48
|
+
const modulePathname = isDashboardContext ? pathname.replace('/dashboard', '') || '/' : pathname
|
|
26
49
|
|
|
27
|
-
//
|
|
50
|
+
// Find matching route
|
|
51
|
+
let routeDef = contextRoutes.find((r) => r.path === pathname)
|
|
52
|
+
if (!routeDef && isDashboardContext) {
|
|
53
|
+
// For dashboard routes, also try matching stripped path
|
|
54
|
+
routeDef = contextRoutes.find((r) => r.path === modulePathname)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check auth for root routes with requiresAuth
|
|
28
58
|
useEffect(() => {
|
|
29
59
|
async function checkAuth() {
|
|
30
60
|
if (!routeDef) {
|
|
@@ -32,7 +62,14 @@ export function KuckitModuleRoute() {
|
|
|
32
62
|
return
|
|
33
63
|
}
|
|
34
64
|
|
|
35
|
-
//
|
|
65
|
+
// Dashboard routes are already protected by the dashboard layout
|
|
66
|
+
if (isDashboardContext) {
|
|
67
|
+
setAuthChecked(true)
|
|
68
|
+
setIsAuthenticated(true)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// For root routes, check if auth is required
|
|
36
73
|
if (routeDef.meta?.requiresAuth) {
|
|
37
74
|
const session = await authClient.getSession()
|
|
38
75
|
if (!session.data) {
|
|
@@ -49,10 +86,10 @@ export function KuckitModuleRoute() {
|
|
|
49
86
|
}
|
|
50
87
|
|
|
51
88
|
checkAuth()
|
|
52
|
-
}, [routeDef, pathname, navigate, authClient])
|
|
89
|
+
}, [routeDef, isDashboardContext, pathname, navigate, authClient])
|
|
53
90
|
|
|
54
|
-
// Show loading while checking auth for protected routes
|
|
55
|
-
if (routeDef?.meta?.requiresAuth && !authChecked) {
|
|
91
|
+
// Show loading while checking auth for protected root routes
|
|
92
|
+
if (!isDashboardContext && routeDef?.meta?.requiresAuth && !authChecked) {
|
|
56
93
|
return (
|
|
57
94
|
<div className="flex items-center justify-center min-h-screen">
|
|
58
95
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
|
@@ -60,8 +97,8 @@ export function KuckitModuleRoute() {
|
|
|
60
97
|
)
|
|
61
98
|
}
|
|
62
99
|
|
|
63
|
-
// Protected route - waiting for redirect
|
|
64
|
-
if (routeDef?.meta?.requiresAuth && !isAuthenticated) {
|
|
100
|
+
// Protected root route - waiting for redirect
|
|
101
|
+
if (!isDashboardContext && routeDef?.meta?.requiresAuth && !isAuthenticated) {
|
|
65
102
|
return null
|
|
66
103
|
}
|
|
67
104
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Link, useRouterState } from '@tanstack/react-router'
|
|
2
|
+
import { LayoutDashboard, Settings, Blocks, type LucideIcon } from 'lucide-react'
|
|
3
|
+
import { useKuckitNav } from '@kuckit/sdk-react'
|
|
4
|
+
import {
|
|
5
|
+
Sidebar,
|
|
6
|
+
SidebarContent,
|
|
7
|
+
SidebarFooter,
|
|
8
|
+
SidebarGroup,
|
|
9
|
+
SidebarGroupContent,
|
|
10
|
+
SidebarGroupLabel,
|
|
11
|
+
SidebarHeader,
|
|
12
|
+
SidebarMenu,
|
|
13
|
+
SidebarMenuButton,
|
|
14
|
+
SidebarMenuItem,
|
|
15
|
+
SidebarRail,
|
|
16
|
+
} from '@/components/ui/sidebar'
|
|
17
|
+
import { NavUser } from './nav-user'
|
|
18
|
+
|
|
19
|
+
const iconMap: Record<string, LucideIcon> = {
|
|
20
|
+
'layout-dashboard': LayoutDashboard,
|
|
21
|
+
settings: Settings,
|
|
22
|
+
blocks: Blocks,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const coreNavItems = [
|
|
26
|
+
{ label: 'Dashboard', path: '/dashboard', icon: LayoutDashboard },
|
|
27
|
+
{ label: 'Settings', path: '/dashboard/settings', icon: Settings },
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
export function AppSidebar() {
|
|
31
|
+
const navRegistry = useKuckitNav()
|
|
32
|
+
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
|
33
|
+
|
|
34
|
+
const mainNavModuleItems = navRegistry.getMainNavItems()
|
|
35
|
+
const moduleItems = navRegistry.getModuleNavItems()
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Sidebar collapsible="icon">
|
|
39
|
+
<SidebarHeader>
|
|
40
|
+
<div className="flex items-center gap-2 px-2 py-2">
|
|
41
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-primary/80">
|
|
42
|
+
<span className="text-sm font-bold text-primary-foreground">K</span>
|
|
43
|
+
</div>
|
|
44
|
+
<span className="font-semibold group-data-[collapsible=icon]:hidden">__APP_TITLE__</span>
|
|
45
|
+
</div>
|
|
46
|
+
</SidebarHeader>
|
|
47
|
+
|
|
48
|
+
<SidebarContent>
|
|
49
|
+
<SidebarGroup>
|
|
50
|
+
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
|
51
|
+
<SidebarGroupContent>
|
|
52
|
+
<SidebarMenu>
|
|
53
|
+
{coreNavItems.map((item) => (
|
|
54
|
+
<SidebarMenuItem key={item.path}>
|
|
55
|
+
<SidebarMenuButton asChild isActive={pathname === item.path} tooltip={item.label}>
|
|
56
|
+
<Link to={item.path}>
|
|
57
|
+
<item.icon className="h-4 w-4" />
|
|
58
|
+
<span>{item.label}</span>
|
|
59
|
+
</Link>
|
|
60
|
+
</SidebarMenuButton>
|
|
61
|
+
</SidebarMenuItem>
|
|
62
|
+
))}
|
|
63
|
+
|
|
64
|
+
{mainNavModuleItems.map((item) => {
|
|
65
|
+
const Icon = item.icon ? iconMap[item.icon] : Blocks
|
|
66
|
+
return (
|
|
67
|
+
<SidebarMenuItem key={item.id}>
|
|
68
|
+
<SidebarMenuButton
|
|
69
|
+
asChild
|
|
70
|
+
isActive={pathname === item.path || pathname.startsWith(item.path + '/')}
|
|
71
|
+
tooltip={item.label}
|
|
72
|
+
>
|
|
73
|
+
<Link to={item.path}>
|
|
74
|
+
{Icon && <Icon className="h-4 w-4" />}
|
|
75
|
+
<span>{item.label}</span>
|
|
76
|
+
</Link>
|
|
77
|
+
</SidebarMenuButton>
|
|
78
|
+
</SidebarMenuItem>
|
|
79
|
+
)
|
|
80
|
+
})}
|
|
81
|
+
</SidebarMenu>
|
|
82
|
+
</SidebarGroupContent>
|
|
83
|
+
</SidebarGroup>
|
|
84
|
+
|
|
85
|
+
{moduleItems.length > 0 && (
|
|
86
|
+
<SidebarGroup>
|
|
87
|
+
<SidebarGroupLabel>Modules</SidebarGroupLabel>
|
|
88
|
+
<SidebarGroupContent>
|
|
89
|
+
<SidebarMenu>
|
|
90
|
+
{moduleItems.map((item) => {
|
|
91
|
+
const Icon = item.icon ? iconMap[item.icon] : Blocks
|
|
92
|
+
return (
|
|
93
|
+
<SidebarMenuItem key={item.id}>
|
|
94
|
+
<SidebarMenuButton
|
|
95
|
+
asChild
|
|
96
|
+
isActive={pathname === item.path || pathname.startsWith(item.path + '/')}
|
|
97
|
+
tooltip={item.label}
|
|
98
|
+
>
|
|
99
|
+
<Link to={item.path}>
|
|
100
|
+
{Icon && <Icon className="h-4 w-4" />}
|
|
101
|
+
<span>{item.label}</span>
|
|
102
|
+
</Link>
|
|
103
|
+
</SidebarMenuButton>
|
|
104
|
+
</SidebarMenuItem>
|
|
105
|
+
)
|
|
106
|
+
})}
|
|
107
|
+
</SidebarMenu>
|
|
108
|
+
</SidebarGroupContent>
|
|
109
|
+
</SidebarGroup>
|
|
110
|
+
)}
|
|
111
|
+
</SidebarContent>
|
|
112
|
+
|
|
113
|
+
<SidebarFooter>
|
|
114
|
+
<NavUser />
|
|
115
|
+
</SidebarFooter>
|
|
116
|
+
|
|
117
|
+
<SidebarRail />
|
|
118
|
+
</Sidebar>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { SidebarProvider, SidebarInset, SidebarTrigger } from '@/components/ui/sidebar'
|
|
3
|
+
import { AppSidebar } from './app-sidebar'
|
|
4
|
+
import { Separator } from '@/components/ui/separator'
|
|
5
|
+
import {
|
|
6
|
+
Breadcrumb,
|
|
7
|
+
BreadcrumbItem,
|
|
8
|
+
BreadcrumbList,
|
|
9
|
+
BreadcrumbPage,
|
|
10
|
+
} from '@/components/ui/breadcrumb'
|
|
11
|
+
import { useRouterState } from '@tanstack/react-router'
|
|
12
|
+
|
|
13
|
+
interface DashboardLayoutProps {
|
|
14
|
+
children: ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function DashboardLayout({ children }: DashboardLayoutProps) {
|
|
18
|
+
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
|
19
|
+
|
|
20
|
+
const pathSegments = pathname.split('/').filter(Boolean)
|
|
21
|
+
const pageTitle =
|
|
22
|
+
pathSegments.length > 1
|
|
23
|
+
? pathSegments[pathSegments.length - 1].charAt(0).toUpperCase() +
|
|
24
|
+
pathSegments[pathSegments.length - 1].slice(1)
|
|
25
|
+
: 'Dashboard'
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<SidebarProvider>
|
|
29
|
+
<AppSidebar />
|
|
30
|
+
<SidebarInset>
|
|
31
|
+
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
|
|
32
|
+
<SidebarTrigger className="-ml-1" />
|
|
33
|
+
<Separator orientation="vertical" className="mr-2 h-4" />
|
|
34
|
+
<Breadcrumb>
|
|
35
|
+
<BreadcrumbList>
|
|
36
|
+
<BreadcrumbItem>
|
|
37
|
+
<BreadcrumbPage>{pageTitle}</BreadcrumbPage>
|
|
38
|
+
</BreadcrumbItem>
|
|
39
|
+
</BreadcrumbList>
|
|
40
|
+
</Breadcrumb>
|
|
41
|
+
</header>
|
|
42
|
+
<main className="flex-1 overflow-auto p-4">{children}</main>
|
|
43
|
+
</SidebarInset>
|
|
44
|
+
</SidebarProvider>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useServices } from '@/providers/ServicesProvider'
|
|
2
|
+
|
|
3
|
+
export function DashboardOverview() {
|
|
4
|
+
const { authClient } = useServices()
|
|
5
|
+
const { data: session } = authClient.useSession()
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="space-y-6">
|
|
9
|
+
<div>
|
|
10
|
+
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
|
|
11
|
+
<p className="text-muted-foreground">Welcome back, {session?.user?.name || 'User'}</p>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
15
|
+
<div className="rounded-lg border bg-card p-6">
|
|
16
|
+
<h3 className="font-semibold">Getting Started</h3>
|
|
17
|
+
<p className="text-sm text-muted-foreground">
|
|
18
|
+
Your Kuckit application is ready. Start building!
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ChevronsUpDown, LogOut, User } from 'lucide-react'
|
|
2
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
3
|
+
import { useServices } from '@/providers/ServicesProvider'
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuSeparator,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from '@/components/ui/dropdown-menu'
|
|
11
|
+
import {
|
|
12
|
+
SidebarMenu,
|
|
13
|
+
SidebarMenuButton,
|
|
14
|
+
SidebarMenuItem,
|
|
15
|
+
useSidebar,
|
|
16
|
+
} from '@/components/ui/sidebar'
|
|
17
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
18
|
+
|
|
19
|
+
export function NavUser() {
|
|
20
|
+
const { isMobile } = useSidebar()
|
|
21
|
+
const navigate = useNavigate()
|
|
22
|
+
const { authClient } = useServices()
|
|
23
|
+
const { data: session } = authClient.useSession()
|
|
24
|
+
|
|
25
|
+
const user = session?.user
|
|
26
|
+
const initials =
|
|
27
|
+
user?.name
|
|
28
|
+
?.split(' ')
|
|
29
|
+
.map((n) => n[0])
|
|
30
|
+
.join('')
|
|
31
|
+
.toUpperCase() || 'U'
|
|
32
|
+
|
|
33
|
+
const handleLogout = async () => {
|
|
34
|
+
await authClient.signOut()
|
|
35
|
+
navigate({ to: '/' })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<SidebarMenu>
|
|
40
|
+
<SidebarMenuItem>
|
|
41
|
+
<DropdownMenu>
|
|
42
|
+
<DropdownMenuTrigger asChild>
|
|
43
|
+
<SidebarMenuButton size="lg" className="data-[state=open]:bg-sidebar-accent">
|
|
44
|
+
<Avatar className="h-8 w-8 rounded-lg">
|
|
45
|
+
<AvatarImage src={user?.image || undefined} alt={user?.name || ''} />
|
|
46
|
+
<AvatarFallback className="rounded-lg bg-gradient-to-br from-primary to-primary/80 text-primary-foreground">
|
|
47
|
+
{initials}
|
|
48
|
+
</AvatarFallback>
|
|
49
|
+
</Avatar>
|
|
50
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
51
|
+
<span className="truncate font-semibold">{user?.name || 'User'}</span>
|
|
52
|
+
<span className="truncate text-xs text-muted-foreground">{user?.email || ''}</span>
|
|
53
|
+
</div>
|
|
54
|
+
<ChevronsUpDown className="ml-auto size-4" />
|
|
55
|
+
</SidebarMenuButton>
|
|
56
|
+
</DropdownMenuTrigger>
|
|
57
|
+
<DropdownMenuContent
|
|
58
|
+
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
|
59
|
+
side={isMobile ? 'bottom' : 'right'}
|
|
60
|
+
align="end"
|
|
61
|
+
sideOffset={4}
|
|
62
|
+
>
|
|
63
|
+
<DropdownMenuItem onClick={() => navigate({ to: '/dashboard/settings' })}>
|
|
64
|
+
<User className="mr-2 h-4 w-4" />
|
|
65
|
+
Settings
|
|
66
|
+
</DropdownMenuItem>
|
|
67
|
+
<DropdownMenuSeparator />
|
|
68
|
+
<DropdownMenuItem onClick={handleLogout}>
|
|
69
|
+
<LogOut className="mr-2 h-4 w-4" />
|
|
70
|
+
Log out
|
|
71
|
+
</DropdownMenuItem>
|
|
72
|
+
</DropdownMenuContent>
|
|
73
|
+
</DropdownMenu>
|
|
74
|
+
</SidebarMenuItem>
|
|
75
|
+
</SidebarMenu>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
|
|
6
|
+
function Avatar({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
|
7
|
+
return (
|
|
8
|
+
<AvatarPrimitive.Root
|
|
9
|
+
data-slot="avatar"
|
|
10
|
+
className={cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', className)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
|
17
|
+
return (
|
|
18
|
+
<AvatarPrimitive.Image
|
|
19
|
+
data-slot="avatar-image"
|
|
20
|
+
className={cn('aspect-square size-full', className)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function AvatarFallback({
|
|
27
|
+
className,
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
|
30
|
+
return (
|
|
31
|
+
<AvatarPrimitive.Fallback
|
|
32
|
+
data-slot="avatar-fallback"
|
|
33
|
+
className={cn('bg-muted flex size-full items-center justify-center rounded-full', className)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { Avatar, AvatarImage, AvatarFallback }
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot'
|
|
3
|
+
import { ChevronRight, MoreHorizontal } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
|
|
7
|
+
function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
|
|
8
|
+
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
|
|
12
|
+
return (
|
|
13
|
+
<ol
|
|
14
|
+
data-slot="breadcrumb-list"
|
|
15
|
+
className={cn(
|
|
16
|
+
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
|
|
25
|
+
return (
|
|
26
|
+
<li
|
|
27
|
+
data-slot="breadcrumb-item"
|
|
28
|
+
className={cn('inline-flex items-center gap-1.5', className)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function BreadcrumbLink({
|
|
35
|
+
asChild,
|
|
36
|
+
className,
|
|
37
|
+
...props
|
|
38
|
+
}: React.ComponentProps<'a'> & {
|
|
39
|
+
asChild?: boolean
|
|
40
|
+
}) {
|
|
41
|
+
const Comp = asChild ? Slot : 'a'
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Comp
|
|
45
|
+
data-slot="breadcrumb-link"
|
|
46
|
+
className={cn('hover:text-foreground transition-colors', className)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
|
|
53
|
+
return (
|
|
54
|
+
<span
|
|
55
|
+
data-slot="breadcrumb-page"
|
|
56
|
+
role="link"
|
|
57
|
+
aria-disabled="true"
|
|
58
|
+
aria-current="page"
|
|
59
|
+
className={cn('text-foreground font-normal', className)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<'li'>) {
|
|
66
|
+
return (
|
|
67
|
+
<li
|
|
68
|
+
data-slot="breadcrumb-separator"
|
|
69
|
+
role="presentation"
|
|
70
|
+
aria-hidden="true"
|
|
71
|
+
className={cn('[&>svg]:size-3.5', className)}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
{children ?? <ChevronRight />}
|
|
75
|
+
</li>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
|
|
80
|
+
return (
|
|
81
|
+
<span
|
|
82
|
+
data-slot="breadcrumb-ellipsis"
|
|
83
|
+
role="presentation"
|
|
84
|
+
aria-hidden="true"
|
|
85
|
+
className={cn('flex size-9 items-center justify-center', className)}
|
|
86
|
+
{...props}
|
|
87
|
+
>
|
|
88
|
+
<MoreHorizontal className="size-4" />
|
|
89
|
+
<span className="sr-only">More</span>
|
|
90
|
+
</span>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
Breadcrumb,
|
|
96
|
+
BreadcrumbList,
|
|
97
|
+
BreadcrumbItem,
|
|
98
|
+
BreadcrumbLink,
|
|
99
|
+
BreadcrumbPage,
|
|
100
|
+
BreadcrumbSeparator,
|
|
101
|
+
BreadcrumbEllipsis,
|
|
102
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'
|
|
4
|
+
|
|
5
|
+
function Collapsible({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
|
6
|
+
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function CollapsibleTrigger({
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
|
12
|
+
return <CollapsiblePrimitive.CollapsibleTrigger data-slot="collapsible-trigger" {...props} />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function CollapsibleContent({
|
|
16
|
+
...props
|
|
17
|
+
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
|
18
|
+
return <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as SeparatorPrimitive from '@radix-ui/react-separator'
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
|
|
6
|
+
function Separator({
|
|
7
|
+
className,
|
|
8
|
+
orientation = 'horizontal',
|
|
9
|
+
decorative = true,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<SeparatorPrimitive.Root
|
|
14
|
+
data-slot="separator"
|
|
15
|
+
decorative={decorative}
|
|
16
|
+
orientation={orientation}
|
|
17
|
+
className={cn(
|
|
18
|
+
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { Separator }
|