create-mantiq 0.7.0 → 0.7.2
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 +2 -1
- package/skeleton/.env.example +64 -0
- package/skeleton/README.md +46 -0
- package/skeleton/app/Console/Commands/.gitkeep +0 -0
- package/skeleton/app/Enums/UserStatus.ts +7 -0
- package/skeleton/app/Http/Controllers/HomeController.ts +78 -0
- package/skeleton/app/Http/Middleware/.gitkeep +0 -0
- package/skeleton/app/Models/User.ts +7 -0
- package/skeleton/app/Providers/AppServiceProvider.ts +25 -0
- package/skeleton/app/Providers/DatabaseServiceProvider.ts +17 -0
- package/skeleton/bootstrap/.gitkeep +0 -0
- package/skeleton/config/ai.ts +51 -0
- package/skeleton/config/app.ts +108 -0
- package/skeleton/config/auth.ts +51 -0
- package/skeleton/config/broadcasting.ts +93 -0
- package/skeleton/config/cache.ts +61 -0
- package/skeleton/config/cors.ts +77 -0
- package/skeleton/config/database.ts +120 -0
- package/skeleton/config/filesystem.ts +58 -0
- package/skeleton/config/hashing.ts +47 -0
- package/skeleton/config/heartbeat.ts +112 -0
- package/skeleton/config/logging.ts +58 -0
- package/skeleton/config/mail.ts +93 -0
- package/skeleton/config/notify.ts +141 -0
- package/skeleton/config/queue.ts +59 -0
- package/skeleton/config/search.ts +96 -0
- package/skeleton/config/services.ts +110 -0
- package/skeleton/config/session.ts +84 -0
- package/skeleton/config/vite.ts +33 -0
- package/skeleton/database/factories/.gitkeep +0 -0
- package/skeleton/database/migrations/001_create_users_table.ts +19 -0
- package/skeleton/database/migrations/002_create_personal_access_tokens_table.ts +22 -0
- package/skeleton/database/seeders/DatabaseSeeder.ts +7 -0
- package/skeleton/index.ts +20 -0
- package/skeleton/mantiq.ts +8 -0
- package/skeleton/package.json +34 -0
- package/skeleton/public/.gitkeep +0 -0
- package/skeleton/routes/api.ts +8 -0
- package/skeleton/routes/channels.ts +23 -0
- package/skeleton/routes/console.ts +24 -0
- package/skeleton/routes/web.ts +6 -0
- package/skeleton/storage/cache/.gitkeep +0 -0
- package/skeleton/storage/framework/.gitkeep +0 -0
- package/skeleton/tests/feature/api.test.ts +14 -0
- package/skeleton/tests/feature/home.test.ts +17 -0
- package/skeleton/tests/unit/example.test.ts +11 -0
- package/skeleton/tsconfig.json +27 -0
- package/src/index.ts +289 -25
- package/src/templates.ts +141 -945
- package/src/terminal.ts +64 -0
- package/stubs/api-only/routes/api.ts.stub +24 -0
- package/stubs/api-only/tests/feature/token-auth.test.ts.stub +69 -0
- package/stubs/auth/api/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
- package/stubs/auth/api/routes/api.ts.stub +24 -0
- package/stubs/auth/api/tests/feature/token-auth.test.ts.stub +69 -0
- package/stubs/auth/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
- package/stubs/auth/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
- package/stubs/auth/web/app/Http/Controllers/AuthController.ts.stub +43 -0
- package/stubs/auth/web/app/Http/Controllers/PageController.ts.stub +66 -0
- package/stubs/auth/web/routes/web.ts.stub +25 -0
- package/stubs/auth/web/svelte/src/App.svelte.stub +77 -0
- package/stubs/auth/web/svelte/src/pages.ts.stub +17 -0
- package/stubs/auth/web/tests/feature/auth.test.ts.stub +69 -0
- package/stubs/auth/web/vue/src/App.vue.stub +74 -0
- package/stubs/auth/web/vue/src/pages.ts.stub +17 -0
- package/stubs/manifest.json +630 -2
- package/stubs/noauth/app/Http/Controllers/PageController.ts.stub +41 -0
- package/stubs/noauth/app/Models/User.ts.stub +5 -0
- package/stubs/noauth/database/migrations/001_create_users_table.ts.stub +17 -0
- package/stubs/noauth/routes/api.ts.stub +16 -0
- package/stubs/noauth/routes/web.ts.stub +15 -0
- package/stubs/noauth/svelte/src/App.svelte.stub +68 -0
- package/stubs/noauth/svelte/src/pages.ts.stub +7 -0
- package/stubs/noauth/vue/src/App.vue.stub +62 -0
- package/stubs/noauth/vue/src/pages.ts.stub +7 -0
- package/stubs/react/src/App.tsx.stub +4 -2
- package/stubs/react/src/components/layout/search-dialog.tsx.stub +2 -2
- package/stubs/react/src/components/layout/sidebar-data.ts.stub +2 -2
- package/stubs/react/src/lib/api.ts.stub +30 -6
- package/stubs/react/src/pages/Login.tsx.stub +3 -3
- package/stubs/react/src/pages/users/dialogs.tsx.stub +7 -26
- package/stubs/react/vite.config.ts.stub +26 -3
- package/stubs/shared/app/Http/Controllers/ApiAuthController.ts.stub +57 -0
- package/stubs/shared/app/Http/Controllers/AuthController.ts.stub +14 -38
- package/stubs/shared/app/Http/Controllers/PageController.ts.stub +3 -3
- package/stubs/shared/app/Http/Controllers/UserController.ts.stub +61 -0
- package/stubs/shared/app/Http/Requests/LoginRequest.ts.stub +10 -0
- package/stubs/shared/app/Http/Requests/RegisterRequest.ts.stub +11 -0
- package/stubs/shared/app/Http/Requests/StoreUserRequest.ts.stub +11 -0
- package/stubs/shared/app/Http/Requests/UpdateUserRequest.ts.stub +11 -0
- package/stubs/shared/config/app.ts.stub +36 -0
- package/stubs/shared/config/vite.ts.stub +8 -0
- package/stubs/shared/database/factories/UserFactory.ts.stub +4 -6
- package/stubs/shared/routes/api.ts.stub +12 -102
- package/stubs/shared/routes/web.ts.stub +5 -3
- package/stubs/shared/tests/feature/auth.test.ts.stub +69 -0
- package/stubs/shared/tests/feature/users.test.ts.stub +90 -0
- package/stubs/svelte/src/App.svelte.stub +1 -1
- package/stubs/svelte/src/lib/api.ts.stub +30 -6
- package/stubs/svelte/src/main.ts.stub +3 -1
- package/stubs/svelte/src/pages/Login.svelte.stub +3 -3
- package/stubs/svelte/vite.config.ts.stub +20 -1
- package/stubs/tailwind-only/react/src/components/layout/app-sidebar.tsx.stub +68 -0
- package/stubs/tailwind-only/react/src/components/layout/authenticated-layout.tsx.stub +57 -0
- package/stubs/tailwind-only/react/src/components/layout/header.tsx.stub +52 -0
- package/stubs/tailwind-only/react/src/components/layout/main.tsx.stub +21 -0
- package/stubs/tailwind-only/react/src/components/layout/nav-group.tsx.stub +185 -0
- package/stubs/tailwind-only/react/src/components/layout/nav-user.tsx.stub +106 -0
- package/stubs/tailwind-only/react/src/components/layout/sidebar-data.ts.stub +58 -0
- package/stubs/tailwind-only/react/src/components/layout/theme-toggle.tsx.stub +36 -0
- package/stubs/tailwind-only/react/src/components/layout/top-nav.tsx.stub +72 -0
- package/stubs/tailwind-only/react/src/lib/utils.ts.stub +6 -0
- package/stubs/tailwind-only/react/src/pages/Dashboard.tsx.stub +205 -0
- package/stubs/tailwind-only/react/src/pages/Login.tsx.stub +122 -0
- package/stubs/tailwind-only/react/src/pages/Register.tsx.stub +137 -0
- package/stubs/tailwind-only/react/src/pages/Users.tsx.stub +426 -0
- package/stubs/tailwind-only/react/src/pages/account/layout.tsx.stub +64 -0
- package/stubs/tailwind-only/react/src/pages/account/preferences.tsx.stub +80 -0
- package/stubs/tailwind-only/react/src/pages/account/profile.tsx.stub +67 -0
- package/stubs/tailwind-only/react/src/pages/account/security.tsx.stub +91 -0
- package/stubs/tailwind-only/react/src/style.css.stub +14 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/app-sidebar.svelte.stub +104 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/authenticated-layout.svelte.stub +51 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/header.svelte.stub +66 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/main.svelte.stub +26 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-group.svelte.stub +131 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/nav-user.svelte.stub +104 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/sidebar-data.ts.stub +57 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/theme-toggle.svelte.stub +28 -0
- package/stubs/tailwind-only/svelte/src/lib/components/layout/top-nav.svelte.stub +99 -0
- package/stubs/tailwind-only/svelte/src/lib/utils.ts.stub +6 -0
- package/stubs/tailwind-only/svelte/src/pages/Dashboard.svelte.stub +192 -0
- package/stubs/tailwind-only/svelte/src/pages/Login.svelte.stub +120 -0
- package/stubs/tailwind-only/svelte/src/pages/Register.svelte.stub +134 -0
- package/stubs/tailwind-only/svelte/src/pages/Users.svelte.stub +293 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Layout.svelte.stub +61 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Preferences.svelte.stub +81 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Profile.svelte.stub +76 -0
- package/stubs/tailwind-only/svelte/src/pages/account/Security.svelte.stub +140 -0
- package/stubs/tailwind-only/svelte/src/style.css.stub +127 -0
- package/stubs/tailwind-only/vue/src/components/layout/AppSidebar.vue.stub +60 -0
- package/stubs/tailwind-only/vue/src/components/layout/AuthenticatedLayout.vue.stub +73 -0
- package/stubs/tailwind-only/vue/src/components/layout/Header.vue.stub +54 -0
- package/stubs/tailwind-only/vue/src/components/layout/Main.vue.stub +22 -0
- package/stubs/tailwind-only/vue/src/components/layout/NavGroup.vue.stub +107 -0
- package/stubs/tailwind-only/vue/src/components/layout/NavUser.vue.stub +104 -0
- package/stubs/tailwind-only/vue/src/components/layout/ThemeToggle.vue.stub +28 -0
- package/stubs/tailwind-only/vue/src/components/layout/TopNav.vue.stub +86 -0
- package/stubs/tailwind-only/vue/src/components/layout/sidebar-data.ts.stub +57 -0
- package/stubs/tailwind-only/vue/src/lib/utils.ts.stub +7 -0
- package/stubs/tailwind-only/vue/src/pages/Dashboard.vue.stub +195 -0
- package/stubs/tailwind-only/vue/src/pages/Login.vue.stub +121 -0
- package/stubs/tailwind-only/vue/src/pages/Register.vue.stub +137 -0
- package/stubs/tailwind-only/vue/src/pages/Users.vue.stub +401 -0
- package/stubs/tailwind-only/vue/src/pages/account/Layout.vue.stub +56 -0
- package/stubs/tailwind-only/vue/src/pages/account/Preferences.vue.stub +81 -0
- package/stubs/tailwind-only/vue/src/pages/account/Profile.vue.stub +69 -0
- package/stubs/tailwind-only/vue/src/pages/account/Security.vue.stub +85 -0
- package/stubs/tailwind-only/vue/src/style.css.stub +26 -0
- package/stubs/themes/corporate/react/src/components/layout/app-sidebar.tsx.stub +74 -0
- package/stubs/themes/corporate/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/corporate/react/src/pages/Dashboard.tsx.stub +310 -0
- package/stubs/themes/corporate/react/src/pages/Login.tsx.stub +122 -0
- package/stubs/themes/corporate/react/src/pages/Register.tsx.stub +144 -0
- package/stubs/themes/corporate/react/src/style.css.stub +135 -0
- package/stubs/themes/corporate/svelte/src/pages/Dashboard.svelte.stub +271 -0
- package/stubs/themes/corporate/svelte/src/pages/Login.svelte.stub +117 -0
- package/stubs/themes/corporate/svelte/src/pages/Register.svelte.stub +138 -0
- package/stubs/themes/corporate/svelte/src/style.css.stub +134 -0
- package/stubs/themes/corporate/vue/src/pages/Dashboard.vue.stub +325 -0
- package/stubs/themes/corporate/vue/src/pages/Login.vue.stub +118 -0
- package/stubs/themes/corporate/vue/src/pages/Register.vue.stub +139 -0
- package/stubs/themes/corporate/vue/src/style.css.stub +134 -0
- package/stubs/themes/minimal/react/src/components/layout/app-sidebar.tsx.stub +68 -0
- package/stubs/themes/minimal/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/minimal/react/src/pages/Dashboard.tsx.stub +166 -0
- package/stubs/themes/minimal/react/src/pages/Login.tsx.stub +95 -0
- package/stubs/themes/minimal/react/src/pages/Register.tsx.stub +73 -0
- package/stubs/themes/minimal/react/src/style.css.stub +142 -0
- package/stubs/themes/minimal/svelte/src/pages/Dashboard.svelte.stub +149 -0
- package/stubs/themes/minimal/svelte/src/pages/Login.svelte.stub +90 -0
- package/stubs/themes/minimal/svelte/src/pages/Register.svelte.stub +70 -0
- package/stubs/themes/minimal/svelte/src/style.css.stub +142 -0
- package/stubs/themes/minimal/vue/src/pages/Dashboard.vue.stub +163 -0
- package/stubs/themes/minimal/vue/src/pages/Login.vue.stub +91 -0
- package/stubs/themes/minimal/vue/src/pages/Register.vue.stub +73 -0
- package/stubs/themes/minimal/vue/src/style.css.stub +142 -0
- package/stubs/themes/starter/react/src/components/layout/app-sidebar.tsx.stub +74 -0
- package/stubs/themes/starter/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/starter/react/src/pages/Dashboard.tsx.stub +236 -0
- package/stubs/themes/starter/react/src/pages/Login.tsx.stub +131 -0
- package/stubs/themes/starter/react/src/pages/Register.tsx.stub +145 -0
- package/stubs/themes/starter/react/src/style.css.stub +141 -0
- package/stubs/themes/starter/svelte/src/pages/Dashboard.svelte.stub +212 -0
- package/stubs/themes/starter/svelte/src/pages/Login.svelte.stub +126 -0
- package/stubs/themes/starter/svelte/src/pages/Register.svelte.stub +139 -0
- package/stubs/themes/starter/svelte/src/style.css.stub +141 -0
- package/stubs/themes/starter/vue/src/pages/Dashboard.vue.stub +228 -0
- package/stubs/themes/starter/vue/src/pages/Login.vue.stub +127 -0
- package/stubs/themes/starter/vue/src/pages/Register.vue.stub +140 -0
- package/stubs/themes/starter/vue/src/style.css.stub +141 -0
- package/stubs/themes/workspace/react/src/components/layout/app-sidebar.tsx.stub +97 -0
- package/stubs/themes/workspace/react/src/components/layout/authenticated-layout.tsx.stub +42 -0
- package/stubs/themes/workspace/react/src/pages/Dashboard.tsx.stub +304 -0
- package/stubs/themes/workspace/react/src/pages/Login.tsx.stub +131 -0
- package/stubs/themes/workspace/react/src/pages/Register.tsx.stub +82 -0
- package/stubs/themes/workspace/react/src/style.css.stub +138 -0
- package/stubs/themes/workspace/svelte/src/pages/Dashboard.svelte.stub +215 -0
- package/stubs/themes/workspace/svelte/src/pages/Login.svelte.stub +124 -0
- package/stubs/themes/workspace/svelte/src/pages/Register.svelte.stub +76 -0
- package/stubs/themes/workspace/svelte/src/style.css.stub +134 -0
- package/stubs/themes/workspace/vue/src/pages/Dashboard.vue.stub +220 -0
- package/stubs/themes/workspace/vue/src/pages/Login.vue.stub +128 -0
- package/stubs/themes/workspace/vue/src/pages/Register.vue.stub +80 -0
- package/stubs/themes/workspace/vue/src/style.css.stub +134 -0
- package/stubs/vue/src/App.vue.stub +2 -1
- package/stubs/vue/src/lib/api.ts.stub +30 -6
- package/stubs/vue/src/main.ts.stub +3 -1
- package/stubs/vue/src/pages/Login.vue.stub +3 -3
- package/stubs/vue/vite.config.ts.stub +20 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
import { ThemeToggle } from '@/components/layout/theme-toggle'
|
|
4
|
+
import { useSidebarToggle } from '@/components/layout/authenticated-layout'
|
|
5
|
+
|
|
6
|
+
interface HeaderProps extends React.HTMLAttributes<HTMLElement> {
|
|
7
|
+
fixed?: boolean
|
|
8
|
+
navigate?: (href: string) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Header({ className, fixed, children, navigate, ...props }: HeaderProps) {
|
|
12
|
+
const [offset, setOffset] = useState(0)
|
|
13
|
+
const { toggle } = useSidebarToggle()
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const onScroll = () => setOffset(document.body.scrollTop || document.documentElement.scrollTop)
|
|
17
|
+
document.addEventListener('scroll', onScroll, { passive: true })
|
|
18
|
+
return () => document.removeEventListener('scroll', onScroll)
|
|
19
|
+
}, [])
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<header
|
|
23
|
+
className={cn(
|
|
24
|
+
'flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear',
|
|
25
|
+
fixed && 'sticky top-0 z-10 bg-white dark:bg-gray-950',
|
|
26
|
+
offset > 10 && fixed ? 'border-b border-gray-200 dark:border-gray-800' : '',
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<div className="flex w-full items-center gap-2 px-4 lg:px-6">
|
|
32
|
+
{/* Sidebar toggle */}
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
onClick={toggle}
|
|
36
|
+
className="-ml-1 inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
|
|
37
|
+
>
|
|
38
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
|
|
39
|
+
<rect width="18" height="18" x="3" y="3" rx="2" />
|
|
40
|
+
<path d="M9 3v18" />
|
|
41
|
+
</svg>
|
|
42
|
+
<span className="sr-only">Toggle sidebar</span>
|
|
43
|
+
</button>
|
|
44
|
+
<div className="mx-1 hidden h-4 w-px bg-gray-200 dark:bg-gray-700 md:block" />
|
|
45
|
+
{children}
|
|
46
|
+
<div className="ms-auto flex items-center gap-2">
|
|
47
|
+
<ThemeToggle />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</header>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
interface MainProps extends React.HTMLAttributes<HTMLElement> {
|
|
4
|
+
fixed?: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Main({ fixed, className, children, ...props }: MainProps) {
|
|
8
|
+
return (
|
|
9
|
+
<main
|
|
10
|
+
className={cn(
|
|
11
|
+
fixed && 'flex flex-grow flex-col overflow-hidden',
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
<div className="px-4 py-6 lg:px-6">
|
|
17
|
+
{children}
|
|
18
|
+
</div>
|
|
19
|
+
</main>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
import type { NavGroup as NavGroupData } from './sidebar-data'
|
|
4
|
+
|
|
5
|
+
interface NavGroupProps {
|
|
6
|
+
group: NavGroupData
|
|
7
|
+
activePath: string
|
|
8
|
+
navigate: (href: string) => void
|
|
9
|
+
collapsed?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function SvgIcon({ path, className }: { path: string; className?: string }) {
|
|
13
|
+
return (
|
|
14
|
+
<svg
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
16
|
+
viewBox="0 0 24 24"
|
|
17
|
+
fill="none"
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
strokeWidth="2"
|
|
20
|
+
strokeLinecap="round"
|
|
21
|
+
strokeLinejoin="round"
|
|
22
|
+
className={cn('h-4 w-4', className)}
|
|
23
|
+
>
|
|
24
|
+
{path.split(' M').map((d, i) => (
|
|
25
|
+
<path key={i} d={i === 0 ? d : `M${d}`} />
|
|
26
|
+
))}
|
|
27
|
+
</svg>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isActive(itemUrl: string, activePath: string): boolean {
|
|
32
|
+
if (itemUrl === activePath) return true
|
|
33
|
+
const itemBase = itemUrl.split('?')[0]
|
|
34
|
+
const activeBase = activePath.split('?')[0]
|
|
35
|
+
return itemBase === activeBase
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isGroupActive(
|
|
39
|
+
items: NavGroupData['items'][number]['items'],
|
|
40
|
+
activePath: string,
|
|
41
|
+
): boolean {
|
|
42
|
+
if (!items) return false
|
|
43
|
+
return items.some((sub) => isActive(sub.url, activePath))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function NavGroup({ group, activePath, navigate, collapsed }: NavGroupProps) {
|
|
47
|
+
return (
|
|
48
|
+
<div className="px-3 py-2">
|
|
49
|
+
{!collapsed && (
|
|
50
|
+
<h3 className="mb-1 px-2 text-[11px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
|
51
|
+
{group.title}
|
|
52
|
+
</h3>
|
|
53
|
+
)}
|
|
54
|
+
<ul className="space-y-0.5">
|
|
55
|
+
{group.items.map((item) =>
|
|
56
|
+
item.items && item.items.length > 0 ? (
|
|
57
|
+
<CollapsibleNavItem
|
|
58
|
+
key={item.title}
|
|
59
|
+
item={item}
|
|
60
|
+
activePath={activePath}
|
|
61
|
+
navigate={navigate}
|
|
62
|
+
collapsed={collapsed}
|
|
63
|
+
/>
|
|
64
|
+
) : item.external ? (
|
|
65
|
+
<li key={item.title}>
|
|
66
|
+
<a
|
|
67
|
+
href={item.url}
|
|
68
|
+
target="_blank"
|
|
69
|
+
rel="noopener noreferrer"
|
|
70
|
+
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
|
|
71
|
+
title={collapsed ? item.title : undefined}
|
|
72
|
+
>
|
|
73
|
+
<SvgIcon path={item.iconPath} />
|
|
74
|
+
{!collapsed && (
|
|
75
|
+
<>
|
|
76
|
+
<span className="flex-1">{item.title}</span>
|
|
77
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="ml-auto h-3 w-3 text-gray-400">
|
|
78
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3" />
|
|
79
|
+
</svg>
|
|
80
|
+
</>
|
|
81
|
+
)}
|
|
82
|
+
</a>
|
|
83
|
+
</li>
|
|
84
|
+
) : (
|
|
85
|
+
<li key={item.title}>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={() => navigate(item.url)}
|
|
89
|
+
className={cn(
|
|
90
|
+
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',
|
|
91
|
+
isActive(item.url, activePath)
|
|
92
|
+
? 'bg-gray-100 font-medium text-gray-900 dark:bg-gray-800 dark:text-gray-50'
|
|
93
|
+
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800',
|
|
94
|
+
)}
|
|
95
|
+
title={collapsed ? item.title : undefined}
|
|
96
|
+
>
|
|
97
|
+
<SvgIcon path={item.iconPath} />
|
|
98
|
+
{!collapsed && (
|
|
99
|
+
<>
|
|
100
|
+
<span>{item.title}</span>
|
|
101
|
+
{item.badge && (
|
|
102
|
+
<span className="ml-auto rounded bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-600 dark:bg-gray-800 dark:text-gray-400">
|
|
103
|
+
{item.badge}
|
|
104
|
+
</span>
|
|
105
|
+
)}
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
</button>
|
|
109
|
+
</li>
|
|
110
|
+
),
|
|
111
|
+
)}
|
|
112
|
+
</ul>
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function CollapsibleNavItem({
|
|
118
|
+
item,
|
|
119
|
+
activePath,
|
|
120
|
+
navigate,
|
|
121
|
+
collapsed,
|
|
122
|
+
}: {
|
|
123
|
+
item: NavGroupData['items'][number]
|
|
124
|
+
activePath: string
|
|
125
|
+
navigate: (href: string) => void
|
|
126
|
+
collapsed?: boolean
|
|
127
|
+
}) {
|
|
128
|
+
const childActive = isGroupActive(item.items, activePath)
|
|
129
|
+
const [open, setOpen] = useState(childActive)
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<li>
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
onClick={() => collapsed ? navigate(item.url) : setOpen(!open)}
|
|
136
|
+
className={cn(
|
|
137
|
+
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',
|
|
138
|
+
childActive
|
|
139
|
+
? 'bg-gray-100 font-medium text-gray-900 dark:bg-gray-800 dark:text-gray-50'
|
|
140
|
+
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800',
|
|
141
|
+
)}
|
|
142
|
+
title={collapsed ? item.title : undefined}
|
|
143
|
+
>
|
|
144
|
+
<SvgIcon path={item.iconPath} />
|
|
145
|
+
{!collapsed && (
|
|
146
|
+
<>
|
|
147
|
+
<span className="flex-1 text-left">{item.title}</span>
|
|
148
|
+
<svg
|
|
149
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
150
|
+
viewBox="0 0 24 24"
|
|
151
|
+
fill="none"
|
|
152
|
+
stroke="currentColor"
|
|
153
|
+
strokeWidth="2"
|
|
154
|
+
strokeLinecap="round"
|
|
155
|
+
strokeLinejoin="round"
|
|
156
|
+
className={cn('h-4 w-4 transition-transform', open && 'rotate-90')}
|
|
157
|
+
>
|
|
158
|
+
<path d="m9 18 6-6-6-6" />
|
|
159
|
+
</svg>
|
|
160
|
+
</>
|
|
161
|
+
)}
|
|
162
|
+
</button>
|
|
163
|
+
{open && !collapsed && item.items && (
|
|
164
|
+
<ul className="ml-4 mt-0.5 space-y-0.5 border-l border-gray-200 pl-2 dark:border-gray-700">
|
|
165
|
+
{item.items.map((sub) => (
|
|
166
|
+
<li key={sub.title}>
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
onClick={() => navigate(sub.url)}
|
|
170
|
+
className={cn(
|
|
171
|
+
'flex w-full items-center rounded-md px-2 py-1.5 text-sm transition-colors',
|
|
172
|
+
isActive(sub.url, activePath)
|
|
173
|
+
? 'font-medium text-gray-900 dark:text-gray-50'
|
|
174
|
+
: 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-50',
|
|
175
|
+
)}
|
|
176
|
+
>
|
|
177
|
+
{sub.title}
|
|
178
|
+
</button>
|
|
179
|
+
</li>
|
|
180
|
+
))}
|
|
181
|
+
</ul>
|
|
182
|
+
)}
|
|
183
|
+
</li>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface NavUserProps {
|
|
4
|
+
user: {
|
|
5
|
+
name: string
|
|
6
|
+
email: string
|
|
7
|
+
role?: string
|
|
8
|
+
}
|
|
9
|
+
navigate: (href: string) => void
|
|
10
|
+
onLogout: () => void
|
|
11
|
+
collapsed?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getInitials(name: string) {
|
|
15
|
+
return name
|
|
16
|
+
.split(' ')
|
|
17
|
+
.map((n) => n[0])
|
|
18
|
+
.join('')
|
|
19
|
+
.toUpperCase()
|
|
20
|
+
.slice(0, 2)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function NavUser({ user, navigate, onLogout, collapsed }: NavUserProps) {
|
|
24
|
+
const [open, setOpen] = useState(false)
|
|
25
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const onClick = (e: MouseEvent) => {
|
|
29
|
+
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
|
30
|
+
}
|
|
31
|
+
document.addEventListener('mousedown', onClick)
|
|
32
|
+
return () => document.removeEventListener('mousedown', onClick)
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div ref={ref} className="relative px-3 py-2">
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={() => setOpen(!open)}
|
|
40
|
+
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
|
|
41
|
+
>
|
|
42
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gray-100 text-xs font-semibold text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
|
43
|
+
{getInitials(user.name)}
|
|
44
|
+
</div>
|
|
45
|
+
{!collapsed && (
|
|
46
|
+
<>
|
|
47
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
48
|
+
<span className="truncate font-semibold text-gray-900 dark:text-gray-50">{user.name}</span>
|
|
49
|
+
<span className="truncate text-xs text-gray-500 dark:text-gray-400">{user.email}</span>
|
|
50
|
+
</div>
|
|
51
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="ml-auto h-4 w-4 text-gray-400">
|
|
52
|
+
<path d="m7 15 5 5 5-5M7 9l5-5 5 5" />
|
|
53
|
+
</svg>
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
</button>
|
|
57
|
+
|
|
58
|
+
{open && (
|
|
59
|
+
<div className="absolute bottom-full left-3 right-3 z-50 mb-1 rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-900">
|
|
60
|
+
{/* User info */}
|
|
61
|
+
<div className="flex items-center gap-2 px-3 py-2">
|
|
62
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gray-100 text-xs font-semibold text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
|
63
|
+
{getInitials(user.name)}
|
|
64
|
+
</div>
|
|
65
|
+
<div className="grid flex-1 text-sm leading-tight">
|
|
66
|
+
<span className="truncate font-semibold text-gray-900 dark:text-gray-50">{user.name}</span>
|
|
67
|
+
<span className="truncate text-xs text-gray-500 dark:text-gray-400">{user.email}</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div className="my-1 h-px bg-gray-100 dark:bg-gray-800" />
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
onClick={() => { setOpen(false); navigate('/account/profile') }}
|
|
74
|
+
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
|
|
75
|
+
>
|
|
76
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
|
|
77
|
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" />
|
|
78
|
+
</svg>
|
|
79
|
+
Account
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={() => { setOpen(false); navigate('/account?tab=preferences') }}
|
|
84
|
+
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-gray-600 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
|
|
85
|
+
>
|
|
86
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
|
|
87
|
+
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2zM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" />
|
|
88
|
+
</svg>
|
|
89
|
+
Settings
|
|
90
|
+
</button>
|
|
91
|
+
<div className="my-1 h-px bg-gray-100 dark:bg-gray-800" />
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={() => { setOpen(false); onLogout() }}
|
|
95
|
+
className="flex w-full items-center gap-2 px-3 py-1.5 text-sm text-red-600 transition-colors hover:bg-gray-100 dark:text-red-400 dark:hover:bg-gray-800"
|
|
96
|
+
>
|
|
97
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
|
|
98
|
+
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9" />
|
|
99
|
+
</svg>
|
|
100
|
+
Sign out
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface NavItem {
|
|
2
|
+
title: string
|
|
3
|
+
url: string
|
|
4
|
+
/** SVG path(s) for a 24x24 viewBox icon */
|
|
5
|
+
iconPath: string
|
|
6
|
+
badge?: string
|
|
7
|
+
external?: boolean
|
|
8
|
+
items?: NavItem[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface NavGroup {
|
|
12
|
+
title: string
|
|
13
|
+
items: NavItem[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// SVG paths (stroke icons, 24x24)
|
|
17
|
+
const icons = {
|
|
18
|
+
home: 'M3 9.5L12 3l9 6.5V20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.5z M9 21V12h6v9',
|
|
19
|
+
users: 'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2 M9 7a4 4 0 1 0 0-8 4 4 0 0 0 0 8z M22 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75',
|
|
20
|
+
settings: 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
|
|
21
|
+
user: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2 M12 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8z',
|
|
22
|
+
lock: 'M19 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2z M7 11V7a5 5 0 0 1 10 0v4',
|
|
23
|
+
palette: 'M12 2a10 10 0 0 0 0 20 2 2 0 0 0 2-2v-.09a1.65 1.65 0 0 1 3 0v.09a2 2 0 0 0 2 2h.44A10 10 0 0 0 12 2z',
|
|
24
|
+
book: 'M4 19.5A2.5 2.5 0 0 1 6.5 17H20 M4 4.5A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5',
|
|
25
|
+
externalLink: 'M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6 M15 3h6v6 M10 14L21 3',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const sidebarData: NavGroup[] = [
|
|
29
|
+
{
|
|
30
|
+
title: 'General',
|
|
31
|
+
items: [
|
|
32
|
+
{ title: 'Dashboard', url: '/dashboard', iconPath: icons.home },
|
|
33
|
+
{ title: 'Users', url: '/users', iconPath: icons.users },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
title: 'Documentation',
|
|
38
|
+
items: [
|
|
39
|
+
{ title: 'Docs', url: 'https://github.com/mantiqjs/mantiq#readme', iconPath: icons.book, external: true },
|
|
40
|
+
{ title: 'GitHub', url: 'https://github.com/mantiqjs/mantiq', iconPath: icons.externalLink, external: true },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
title: 'Account',
|
|
45
|
+
items: [
|
|
46
|
+
{
|
|
47
|
+
title: 'Settings',
|
|
48
|
+
url: '/account/profile',
|
|
49
|
+
iconPath: icons.settings,
|
|
50
|
+
items: [
|
|
51
|
+
{ title: 'Profile', url: '/account/profile', iconPath: icons.user },
|
|
52
|
+
{ title: 'Security', url: '/account/security', iconPath: icons.lock },
|
|
53
|
+
{ title: 'Preferences', url: '/account/preferences', iconPath: icons.palette },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export function ThemeToggle() {
|
|
4
|
+
const [isDark, setIsDark] = useState(() =>
|
|
5
|
+
typeof document !== 'undefined'
|
|
6
|
+
? document.documentElement.classList.contains('dark')
|
|
7
|
+
: false,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
const toggleTheme = () => {
|
|
11
|
+
const dark = document.documentElement.classList.toggle('dark')
|
|
12
|
+
localStorage.setItem('theme', dark ? 'dark' : 'light')
|
|
13
|
+
setIsDark(dark)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
onClick={toggleTheme}
|
|
20
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-50"
|
|
21
|
+
title={isDark ? 'Light mode' : 'Dark mode'}
|
|
22
|
+
>
|
|
23
|
+
{isDark ? (
|
|
24
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
|
|
25
|
+
<circle cx="12" cy="12" r="4" />
|
|
26
|
+
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
|
|
27
|
+
</svg>
|
|
28
|
+
) : (
|
|
29
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
|
|
30
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
31
|
+
</svg>
|
|
32
|
+
)}
|
|
33
|
+
<span className="sr-only">Toggle theme</span>
|
|
34
|
+
</button>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
interface TopNavLink {
|
|
4
|
+
title: string
|
|
5
|
+
href: string
|
|
6
|
+
isActive?: boolean
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
|
|
11
|
+
links: TopNavLink[]
|
|
12
|
+
onLinkClick?: (href: string) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function TopNav({
|
|
16
|
+
className,
|
|
17
|
+
links,
|
|
18
|
+
onLinkClick,
|
|
19
|
+
...props
|
|
20
|
+
}: TopNavProps) {
|
|
21
|
+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
|
|
22
|
+
if (onLinkClick) {
|
|
23
|
+
e.preventDefault()
|
|
24
|
+
onLinkClick(href)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
{/* Desktop navigation */}
|
|
31
|
+
<nav
|
|
32
|
+
className={cn(
|
|
33
|
+
'hidden items-center gap-4 md:flex lg:gap-6',
|
|
34
|
+
className,
|
|
35
|
+
)}
|
|
36
|
+
{...props}
|
|
37
|
+
>
|
|
38
|
+
{links.map((link) => (
|
|
39
|
+
<a
|
|
40
|
+
key={link.href}
|
|
41
|
+
href={link.href}
|
|
42
|
+
onClick={(e) => handleClick(e, link.href)}
|
|
43
|
+
className={cn(
|
|
44
|
+
'text-sm font-medium transition-colors hover:text-gray-900 dark:hover:text-gray-50',
|
|
45
|
+
link.isActive
|
|
46
|
+
? 'text-gray-900 dark:text-gray-50'
|
|
47
|
+
: 'text-gray-500 dark:text-gray-400',
|
|
48
|
+
link.disabled && 'pointer-events-none opacity-50',
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
{link.title}
|
|
52
|
+
</a>
|
|
53
|
+
))}
|
|
54
|
+
</nav>
|
|
55
|
+
|
|
56
|
+
{/* Mobile navigation */}
|
|
57
|
+
<div className="md:hidden">
|
|
58
|
+
<select
|
|
59
|
+
onChange={(e) => onLinkClick?.(e.target.value)}
|
|
60
|
+
defaultValue={links.find(l => l.isActive)?.href ?? ''}
|
|
61
|
+
className="rounded-md border border-gray-200 bg-white px-3 py-1.5 text-sm dark:border-gray-700 dark:bg-gray-900"
|
|
62
|
+
>
|
|
63
|
+
{links.map((link) => (
|
|
64
|
+
<option key={link.href} value={link.href} disabled={link.disabled}>
|
|
65
|
+
{link.title}
|
|
66
|
+
</option>
|
|
67
|
+
))}
|
|
68
|
+
</select>
|
|
69
|
+
</div>
|
|
70
|
+
</>
|
|
71
|
+
)
|
|
72
|
+
}
|