new admin ui

This commit is contained in:
2025-12-30 17:22:02 -05:00
parent 85d4629d3c
commit 228d08337f
4 changed files with 430 additions and 95 deletions
+48 -17
View File
@@ -3,7 +3,9 @@
import type React from "react";
import { useEffect, useMemo, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import AdminLeftNavigation from "@/components/AdminLeftNavigation";
import AdminLeftNavigation, {
type AdminNavGroup,
} from "@/components/AdminLeftNavigation";
import { useAuth } from "@/context/AuthContext";
import {
LayoutDashboard,
@@ -13,24 +15,53 @@ import {
Store,
Users,
Settings,
LucideMail,
Mail,
Wand2,
FolderKanban,
Shield,
} from "lucide-react";
const navItems = [
{ label: "Dashboard", href: "/admin", icon: <LayoutDashboard className="h-4 w-4" /> },
{ label: "Imports", href: "/admin/import-status", icon: <Download className="h-4 w-4" /> },
{ label: "Mappings", href: "/admin/mapping", icon: <Layers className="h-4 w-4" /> },
{ label: "Products", href: "/admin/products", icon: <Boxes className="h-4 w-4" /> },
{ label: "Merchants", href: "/admin/merchants", icon: <Store className="h-4 w-4" /> },
{ label: "Platforms", href: "/admin/platforms", icon: <Layers className="h-4 w-4" /> },
{ label: "Enrichment", href: "/admin/enrichment", icon: <Wand2 className="h-4 w-4" /> },
{ label: "Users", href: "/admin/users", icon: <Users className="h-4 w-4" /> },
{ label: "Settings", href: "/admin/settings", icon: <Settings className="h-4 w-4" /> },
{ label: "List Emails", href: "/admin/email", icon: <LucideMail className="h-4 w-4" /> },
{ label: "Send an Email", href: "/admin/email/send", icon: <LucideMail className="h-4 w-4" /> },
{ label: "Beta Invites", href: "/admin/beta-invites", icon: <LucideMail className="h-4 w-4" /> },
const navGroups: AdminNavGroup[] = [
{
label: "Overview",
icon: <LayoutDashboard className="h-4 w-4" />,
items: [{ label: "Dashboard", href: "/admin", icon: <LayoutDashboard className="h-4 w-4" /> }],
},
{
label: "Catalog",
icon: <FolderKanban className="h-4 w-4" />,
items: [
{ label: "Products", href: "/admin/products", icon: <Boxes className="h-4 w-4" /> },
{ label: "Platforms", href: "/admin/platforms", icon: <Layers className="h-4 w-4" /> },
],
},
{
label: "Data Ops",
icon: <Download className="h-4 w-4" />,
items: [
{ label: "Imports", href: "/admin/import-status", icon: <Download className="h-4 w-4" /> },
{ label: "Mappings", href: "/admin/mapping", icon: <Layers className="h-4 w-4" /> },
{ label: "Enrichment", href: "/admin/enrichment", icon: <Wand2 className="h-4 w-4" /> },
],
},
{
label: "Growth & Comms",
icon: <Mail className="h-4 w-4" />,
items: [
{ label: "Beta Invites", href: "/admin/beta-invites", icon: <Mail className="h-4 w-4" /> },
{ label: "List Emails", href: "/admin/email", icon: <Mail className="h-4 w-4" /> },
{ label: "Send an Email", href: "/admin/email/send", icon: <Mail className="h-4 w-4" /> },
],
},
{
label: "Access & Config",
icon: <Shield className="h-4 w-4" />,
items: [
{ label: "Users", href: "/admin/users", icon: <Users className="h-4 w-4" /> },
{ label: "Merchants", href: "/admin/merchants", icon: <Store className="h-4 w-4" /> },
{ label: "Settings", href: "/admin/settings", icon: <Settings className="h-4 w-4" /> },
],
},
];
export default function AdminLayout({
@@ -79,7 +110,7 @@ export default function AdminLayout({
<AdminLeftNavigation
collapsed={collapsed}
onToggleCollapsed={() => setCollapsed((v) => !v)}
items={navItems}
groups={navGroups}
/>
{/* Main column */}
+168 -78
View File
@@ -1,109 +1,199 @@
"use client";
import type React from "react";
import * as React from "react";
import Link from "next/link";
import {
LayoutDashboard,
Download,
Layers,
Boxes,
Store,
Users,
Settings,
LucideMail,
Wand2,
} from "lucide-react";
import { usePathname } from "next/navigation";
import { Disclosure, DisclosureButton, DisclosurePanel } from "@headlessui/react";
import { ChevronRight, PanelLeftClose, PanelLeftOpen } from "lucide-react";
export type AdminNavItem = {
label: string;
href: string;
icon: React.ReactNode;
icon?: React.ReactNode;
};
type AdminLeftNavigationProps = {
collapsed: boolean;
onToggleCollapsed: () => void;
items?: AdminNavItem[]; // ✅ NEW
export type AdminNavGroup = {
label: string;
icon?: React.ReactNode;
items: AdminNavItem[];
};
const defaultNavItems: AdminNavItem[] = [
{ label: "Dashboard", href: "/admin", icon: <LayoutDashboard className="h-4 w-4" /> },
{ label: "Imports", href: "/admin/import-status", icon: <Download className="h-4 w-4" /> },
{ label: "Mappings", href: "/admin/mapping", icon: <Layers className="h-4 w-4" /> },
{ label: "Products", href: "/admin/products", icon: <Boxes className="h-4 w-4" /> },
{ label: "Merchants", href: "/admin/merchants", icon: <Store className="h-4 w-4" /> },
{ label: "Platforms", href: "/admin/platforms", icon: <Layers className="h-4 w-4" /> },
{ label: "Enrichment", href: "/admin/enrichment", icon: <Wand2 className="h-4 w-4" /> },
{ label: "Users", href: "/admin/users", icon: <Users className="h-4 w-4" /> },
{ label: "Settings", href: "/admin/settings", icon: <Settings className="h-4 w-4" /> },
{ label: "Manage Emails", href: "/admin/email/manage", icon: <LucideMail className="h-4 w-4" /> },
{ label: "Send an Email", href: "/admin/email/send", icon: <LucideMail className="h-4 w-4" /> },
{ label: "Beta Invites", href: "/admin/beta-invites", icon: <LucideMail className="h-4 w-4" />,
},
];
function cx(...classes: Array<string | false | undefined | null>) {
return classes.filter(Boolean).join(" ");
}
function isActiveRoute(pathname: string, href: string) {
// Exact match for /admin, prefix match for nested pages
if (href === "/admin") return pathname === "/admin";
return pathname === href || pathname.startsWith(`${href}/`);
}
function groupHasActiveItem(pathname: string, group: AdminNavGroup) {
return group.items.some((i) => isActiveRoute(pathname, i.href));
}
export default function AdminLeftNavigation({
collapsed,
onToggleCollapsed,
items = defaultNavItems, // ✅ NEW default
}: AdminLeftNavigationProps) {
groups,
}: {
collapsed: boolean;
onToggleCollapsed: () => void;
groups: AdminNavGroup[];
}) {
const pathname = usePathname() || "/admin";
return (
<aside
className={`hidden border-r border-zinc-900 bg-zinc-950/80 px-3 py-6 md:flex md:flex-col transition-all duration-200 ${
collapsed ? "w-16" : "w-60"
}`}
className={cx(
"sticky top-0 flex h-screen shrink-0 flex-col border-r border-zinc-900 bg-zinc-950/60 backdrop-blur",
collapsed ? "w-[68px]" : "w-[280px]"
)}
>
<div className="mb-6 flex items-center gap-2">
{/* Header / Brand */}
<div className={cx("flex items-center justify-between px-3 py-3", collapsed && "justify-center")}>
<div className={cx("flex items-center gap-2", collapsed && "hidden")}>
<div className="h-8 w-8 rounded-lg bg-zinc-900/70 ring-1 ring-zinc-800" />
<div className="leading-tight">
<div className="text-xs uppercase tracking-[0.22em] text-zinc-500">
Battl
</div>
<div className="text-sm font-medium text-zinc-100">
Admin
</div>
</div>
</div>
<button
type="button"
onClick={onToggleCollapsed}
className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-zinc-800 bg-zinc-950 text-zinc-400 transition hover:border-amber-400/60 hover:text-amber-300"
className={cx(
"inline-flex h-9 w-9 items-center justify-center rounded-md border border-zinc-800 bg-zinc-900/40 text-zinc-200 hover:bg-zinc-900/70",
collapsed && "mx-auto"
)}
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
title={collapsed ? "Expand" : "Collapse"}
>
<span className="sr-only">Toggle sidebar</span>
<div className="space-y-0.5">
<span className="block h-[1px] w-3 bg-current" />
<span className="block h-[1px] w-3 bg-current" />
<span className="block h-[1px] w-3 bg-current" />
</div>
{collapsed ? (
<PanelLeftOpen className="h-4 w-4" />
) : (
<PanelLeftClose className="h-4 w-4" />
)}
</button>
{!collapsed && (
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-500">
<Link href="/" title="Back to Battl Dashboard">
Battl Builders
</Link>
</p>
<p className="text-[10px] text-zinc-600">Admin Command</p>
</div>
)}
</div>
<nav className="flex-1 space-y-1 text-sm">
{items.map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center justify-between rounded-md px-2 py-1.5 text-zinc-300 transition hover:bg-zinc-900 hover:text-amber-300"
>
<div className="flex items-center gap-2">
{item.icon}
{!collapsed && <span>{item.label}</span>}
</div>
{!collapsed && <span className="text-[10px] text-zinc-600"></span>}
</Link>
))}
{/* Nav */}
<nav className="flex-1 overflow-y-auto px-2 pb-3">
<ul className="space-y-1">
{groups.map((group) => {
const defaultOpen = groupHasActiveItem(pathname, group);
// In collapsed mode, we render items as a flat icon list (no accordion)
if (collapsed) {
return (
<li key={group.label} className="pt-1">
<div className="my-2 h-px bg-zinc-900" />
<div className="flex flex-col gap-1">
{group.items.map((item) => {
const active = isActiveRoute(pathname, item.href);
return (
<Link
key={item.href}
href={item.href}
title={`${group.label}${item.label}`}
className={cx(
"flex h-10 items-center justify-center rounded-md border border-transparent",
active
? "bg-white/5 text-white ring-1 ring-zinc-800"
: "text-zinc-400 hover:bg-white/5 hover:text-zinc-200"
)}
>
<span className="inline-flex">{item.icon}</span>
</Link>
);
})}
</div>
</li>
);
}
return (
<li key={group.label}>
<Disclosure defaultOpen={defaultOpen}>
{({ open }) => (
<>
<DisclosureButton
className={cx(
"group flex w-full items-center justify-between rounded-md px-2 py-2 text-left text-xs font-semibold uppercase tracking-[0.18em]",
open ? "bg-white/5 text-zinc-100" : "text-zinc-400 hover:bg-white/5 hover:text-zinc-200"
)}
>
<span className="flex items-center gap-2">
<span className="inline-flex text-zinc-400 group-hover:text-zinc-200">
{group.icon}
</span>
{group.label}
</span>
<ChevronRight
className={cx(
"h-4 w-4 text-zinc-500 transition-transform",
open && "rotate-90"
)}
/>
</DisclosureButton>
<DisclosurePanel className="mt-1 space-y-1 pl-1">
{group.items.map((item) => {
const active = isActiveRoute(pathname, item.href);
return (
<Link
key={item.href}
href={item.href}
className={cx(
"flex items-center gap-2 rounded-md px-2 py-2 text-sm",
active
? "bg-white/5 text-white ring-1 ring-zinc-800"
: "text-zinc-300 hover:bg-white/5 hover:text-white"
)}
>
<span className="inline-flex h-5 w-5 items-center justify-center text-zinc-400">
{item.icon}
</span>
<span className="truncate">{item.label}</span>
</Link>
);
})}
</DisclosurePanel>
</>
)}
</Disclosure>
</li>
);
})}
</ul>
</nav>
{!collapsed && (
<div className="mt-6 border-t border-zinc-900 pt-4 text-[11px] text-zinc-600">
<p className="text-[10px] uppercase tracking-[0.16em] text-zinc-500">Status</p>
<p className="mt-1">
Import engine: <span className="text-emerald-400">online</span>
</p>
<p className="text-zinc-500">Builder UI: in progress</p>
{/* Footer */}
<div className={cx("border-t border-zinc-900 p-3", collapsed && "px-2")}>
<div
className={cx(
"flex items-center gap-3 rounded-md bg-zinc-900/30 px-3 py-2 ring-1 ring-zinc-800",
collapsed && "justify-center px-2"
)}
title="Admin user"
>
<div className="h-8 w-8 rounded-full bg-zinc-900 ring-1 ring-zinc-800" />
{!collapsed && (
<div className="min-w-0">
<div className="text-xs uppercase tracking-[0.18em] text-zinc-500">
Internal
</div>
<div className="truncate text-sm text-zinc-200">ADMIN</div>
</div>
)}
</div>
)}
</div>
</aside>
);
}
+213
View File
@@ -8,6 +8,7 @@
"name": "gunbuilder-prototype",
"version": "0.1.0",
"dependencies": {
"@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.2.0",
"clsx": "^2.1.1",
"lucide-react": "^0.555.0",
@@ -140,6 +141,79 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/react": {
"version": "0.26.28",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.8",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.7.4"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@headlessui/react": {
"version": "2.2.9",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz",
"integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.20.2",
"@react-aria/interactions": "^3.25.0",
"@tanstack/react-virtual": "^3.13.9",
"use-sync-external-store": "^1.5.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/@heroicons/react": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
@@ -505,6 +579,103 @@
"node": ">=14"
}
},
"node_modules/@react-aria/focus": {
"version": "3.21.3",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.3.tgz",
"integrity": "sha512-FsquWvjSCwC2/sBk4b+OqJyONETUIXQ2vM0YdPAuC+QFQh2DT6TIBo6dOZVSezlhudDla69xFBd6JvCFq1AbUw==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.26.0",
"@react-aria/utils": "^3.32.0",
"@react-types/shared": "^3.32.1",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/interactions": {
"version": "3.26.0",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.26.0.tgz",
"integrity": "sha512-AAEcHiltjfbmP1i9iaVw34Mb7kbkiHpYdqieWufldh4aplWgsF11YQZOfaCJW4QoR2ML4Zzoa9nfFwLXA52R7Q==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-aria/utils": "^3.32.0",
"@react-stately/flags": "^3.1.2",
"@react-types/shared": "^3.32.1",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/utils": {
"version": "3.32.0",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.32.0.tgz",
"integrity": "sha512-/7Rud06+HVBIlTwmwmJa2W8xVtgxgzm0+kLbuFooZRzKDON6hhozS1dOMR/YLMxyJOaYOTpImcP4vRR9gL1hEg==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-stately/flags": "^3.1.2",
"@react-stately/utils": "^3.11.0",
"@react-types/shared": "^3.32.1",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-stately/flags": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
"integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@react-stately/utils": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.11.0.tgz",
"integrity": "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-types/shared": {
"version": "3.32.1",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz",
"integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -535,6 +706,33 @@
"tslib": "^2.4.0"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.14",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.14.tgz",
"integrity": "sha512-WG0d7mBD54eA7dgA3+sO5csS0B49QKqM6Gy5Rf31+Oq/LTKROQSao9m2N/vz1IqVragOKU5t5k1LAcqh/DfTxw==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.14"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.14",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.14.tgz",
"integrity": "sha512-b5Uvd8J2dc7ICeX9SRb/wkCxWk7pUwN214eEPAQsqrsktSKTCmyLxOQWSMgogBByXclZeAdgZ3k4o0fIYUIBqQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -5655,6 +5853,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tabbable": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz",
"integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==",
"license": "MIT"
},
"node_modules/tailwindcss": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
@@ -6094,6 +6298,15 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+1
View File
@@ -9,6 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"@headlessui/react": "^2.2.9",
"@heroicons/react": "^2.2.0",
"clsx": "^2.1.1",
"lucide-react": "^0.555.0",