+
- top-reservation
+
top-reservation
-
-
+
+
-
-
{children}
+ {/* pb: clears the fixed mobile bottom-tab bar (~52px + safe-area) so it never covers the last bit of scrolled content */}
+
+ {children}
+
);
}
diff --git a/src/app/(admin)/mobile-nav.tsx b/src/app/(admin)/mobile-nav.tsx
new file mode 100644
index 0000000..783eaf0
--- /dev/null
+++ b/src/app/(admin)/mobile-nav.tsx
@@ -0,0 +1,89 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { MoreHorizontal, X } from "lucide-react";
+import { links, PRIMARY_COUNT } from "./nav-data";
+
+export function MobileNav({ isAdmin }: { isAdmin: boolean }) {
+ const pathname = usePathname();
+ const [moreOpen, setMoreOpen] = useState(false);
+
+ const visibleLinks = links.filter((link) => !link.adminOnly || isAdmin);
+ const primary = visibleLinks.slice(0, PRIMARY_COUNT);
+ const overflow = visibleLinks.slice(PRIMARY_COUNT);
+ const overflowActive = overflow.some((l) => pathname.startsWith(l.href));
+
+ function isActive(link: (typeof links)[number]) {
+ return link.exact ? pathname === link.href : pathname.startsWith(link.href);
+ }
+
+ return (
+ <>
+ {moreOpen && (
+
setMoreOpen(false)}>
+
e.stopPropagation()}
+ >
+
+ Ещё
+ setMoreOpen(false)} className="rounded-md p-1 text-text-faint hover:bg-surface-hover">
+
+
+
+ {overflow.map((link) => {
+ const active = isActive(link);
+ return (
+
setMoreOpen(false)}
+ className={`flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium ${
+ active ? "bg-accent-soft text-accent-soft-text" : "text-text-muted hover:bg-surface-hover hover:text-text"
+ }`}
+ >
+
+ {link.label}
+
+ );
+ })}
+
+
+ )}
+
+
+ {primary.map((link) => {
+ const active = isActive(link);
+ return (
+
+
+ {link.shortLabel ?? link.label}
+
+ );
+ })}
+ {overflow.length > 0 && (
+ setMoreOpen(true)}
+ className={`flex flex-1 flex-col items-center gap-0.5 py-2 text-[11px] font-medium ${
+ overflowActive ? "text-accent" : "text-text-faint"
+ }`}
+ >
+
+ Ещё
+
+ )}
+
+ >
+ );
+}
diff --git a/src/app/(admin)/nav-data.ts b/src/app/(admin)/nav-data.ts
new file mode 100644
index 0000000..595f679
--- /dev/null
+++ b/src/app/(admin)/nav-data.ts
@@ -0,0 +1,25 @@
+import { LayoutDashboard, CalendarPlus, ListChecks, BarChart3, Building2, Users } from "lucide-react";
+
+export interface NavLink {
+ href: string;
+ label: string;
+ /** Shorter label for the mobile bottom-bar slot, where space is tight — falls back to `label` if unset. */
+ shortLabel?: string;
+ icon: typeof LayoutDashboard;
+ adminOnly: boolean;
+ exact: boolean;
+}
+
+// The first 4 are the mobile bottom bar's fixed slots (thumb-reachable,
+// matches exactly what a non-admin sees — no overflow for them at all);
+// admin-only extras live behind "Ещё" on mobile, same as elmi-portal.
+export const links: NavLink[] = [
+ { href: "/", label: "Дашборд", icon: LayoutDashboard, adminOnly: false, exact: true },
+ { href: "/bookings/new", label: "Новое бронирование", shortLabel: "Создать", icon: CalendarPlus, adminOnly: false, exact: true },
+ { href: "/bookings", label: "Бронирования", shortLabel: "Брони", icon: ListChecks, adminOnly: false, exact: false },
+ { href: "/statistics", label: "Статистика", icon: BarChart3, adminOnly: false, exact: false },
+ { href: "/settings/objects", label: "Объекты", icon: Building2, adminOnly: true, exact: false },
+ { href: "/settings/users", label: "Сотрудники", icon: Users, adminOnly: true, exact: false },
+];
+
+export const PRIMARY_COUNT = 4;
diff --git a/src/app/(admin)/nav-links.tsx b/src/app/(admin)/nav-links.tsx
index 9896c88..e4e9bcf 100644
--- a/src/app/(admin)/nav-links.tsx
+++ b/src/app/(admin)/nav-links.tsx
@@ -2,16 +2,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
-import { LayoutDashboard, CalendarPlus, ListChecks, BarChart3, Building2, Users } from "lucide-react";
-
-const links = [
- { href: "/", label: "Дашборд", icon: LayoutDashboard, adminOnly: false, exact: true },
- { href: "/bookings/new", label: "Новое бронирование", icon: CalendarPlus, adminOnly: false, exact: true },
- { href: "/bookings", label: "Бронирования", icon: ListChecks, adminOnly: false, exact: false },
- { href: "/statistics", label: "Статистика", icon: BarChart3, adminOnly: false, exact: false },
- { href: "/settings/objects", label: "Объекты", icon: Building2, adminOnly: true, exact: false },
- { href: "/settings/users", label: "Сотрудники", icon: Users, adminOnly: true, exact: false },
-];
+import { links } from "./nav-data";
export function NavLinks({ isAdmin }: { isAdmin: boolean }) {
const pathname = usePathname();
diff --git a/src/app/(admin)/nav-shell.tsx b/src/app/(admin)/nav-shell.tsx
new file mode 100644
index 0000000..44c40cf
--- /dev/null
+++ b/src/app/(admin)/nav-shell.tsx
@@ -0,0 +1,14 @@
+import { NavLinks } from "./nav-links";
+import { MobileNav } from "./mobile-nav";
+
+/** Desktop top nav (>=lg) + the mobile bottom tab bar (
+
+
+
+
+ >
+ );
+}