diff --git a/src/components/admin/AdminSidebar.tsx b/src/components/admin/AdminSidebar.tsx
index ce553b5..96e1fa9 100644
--- a/src/components/admin/AdminSidebar.tsx
+++ b/src/components/admin/AdminSidebar.tsx
@@ -1,226 +1,146 @@
"use client";
-import { useState, useEffect, useRef, useCallback, KeyboardEvent } from "react";
+import { useState, useEffect, useRef, useCallback, KeyboardEvent, ComponentType } from "react";
import Link from "next/link";
-import { usePathname } from "next/navigation";
-import { useRouter } from "next/navigation";
+import { usePathname, useRouter } from "next/navigation";
+import {
+ LayoutDashboard,
+ ShoppingCart,
+ MapPin,
+ Package,
+ Truck,
+ Ship,
+ Mail,
+ Store,
+ Sparkles,
+ Upload,
+ BrainCircuit,
+ Clock,
+ Droplets,
+ Route,
+ ChartBar,
+ Receipt,
+ Settings,
+ LogOut,
+ Menu,
+ X,
+ ArrowLeft,
+} from "lucide-react";
import { signOutAction } from "@/actions/auth-actions";
import BrandSelector from "@/components/admin/BrandSelector";
+import SideNavGroup from "@/components/admin/SideNavGroup";
-// Elegant warm sidebar design
-// Colors: parchment 100 bg, soft linen text, powder petal accent
+// Sidebar tokens used (from src/styles/admin-design-system.css):
+// --admin-sidebar-bg (#2A2520)
+// --admin-sidebar-text (#B8B4A8)
+// --admin-sidebar-hover (#3A352F)
+// --admin-sidebar-active (#4A443C)
+// --admin-sidebar-accent (#D4A24C — amber, used for active left-border + dot)
+// The "amber accent" replaces the old `--admin-accent` (citrus orange) in the
+// sidebar only — primary buttons elsewhere still use the deep botanical green.
-type NavItem = {
- href?: string;
+type IconComponent = ComponentType<{ className?: string; size?: number | string }>;
+
+type NavItemDef = {
+ href: string;
label: string;
- icon?: string;
- divider?: boolean;
+ icon: IconComponent;
+ /** If set, this item is hidden when `enabledAddons[key] === false`. */
+ addonKey?: string;
+ /** When true, only `userRole === "platform_admin"` sees this item. */
+ requiresPlatformAdmin?: boolean;
};
-const NAV_ITEMS: NavItem[] = [
- // Main
- { href: "/admin", label: "Dashboard", icon: "grid" },
- { href: "/admin/orders", label: "Orders", icon: "shopping-cart" },
- { href: "/admin/stops", label: "Stops & Routes", icon: "map-pin" },
- { href: "/admin/products", label: "Products", icon: "package" },
- { href: "/admin/communications", label: "Communications", icon: "mail" },
- // Settings section
- { divider: true, label: "Settings" },
- { href: "/admin/settings", label: "General", icon: "settings" },
- { href: "/admin/time-tracking", label: "Workers & PINs", icon: "users" },
- { href: "/admin/time-tracking?tab=tasks", label: "Tasks", icon: "clipboard" },
- { href: "/admin/users", label: "Users & Permissions", icon: "shield" },
- { href: "/admin/settings/integrations", label: "Integrations", icon: "plug" },
- { href: "/admin/settings/billing", label: "Billing", icon: "billing" },
+type NavGroup = {
+ label: string;
+ items: NavItemDef[];
+};
+
+/**
+ * Grouped nav IA per `docs/superpowers/specs/2026-06-17-admin-redesign.md` §4.
+ * Order is intentional (top-to-bottom = frequency-of-use + mental flow).
+ *
+ * - Workspace : Dashboard is always shown; Command Center is platform_admin-only.
+ * - Operations : Day-to-day order/stop/product flow.
+ * - Communications: Harvest Reach only — the other sub-items live inside that page.
+ * - Growth : Wholesale portal, data imports, AI assistant.
+ * - Tracking : Time tracking is always on; Water Log / Route Trace are
+ * gated on their respective add-ons via `enabledAddons`.
+ * - Insights : Reporting + tax.
+ * - Settings : Single entry; sub-pages are tabs inside /admin/settings.
+ */
+const NAV_GROUPS: NavGroup[] = [
+ {
+ label: "Workspace",
+ items: [
+ { href: "/admin", label: "Dashboard", icon: LayoutDashboard },
+ {
+ href: "/admin/command-center",
+ label: "Command Center",
+ icon: Sparkles,
+ requiresPlatformAdmin: true,
+ },
+ ],
+ },
+ {
+ label: "Operations",
+ items: [
+ { href: "/admin/orders", label: "Orders", icon: ShoppingCart },
+ { href: "/admin/stops", label: "Stops & Routes", icon: MapPin },
+ { href: "/admin/products", label: "Products", icon: Package },
+ { href: "/admin/pickup", label: "Driver Pickup", icon: Truck },
+ { href: "/admin/shipping", label: "Shipping", icon: Ship },
+ ],
+ },
+ {
+ label: "Communications",
+ items: [
+ { href: "/admin/communications", label: "Harvest Reach", icon: Mail },
+ ],
+ },
+ {
+ label: "Growth",
+ items: [
+ { href: "/admin/wholesale", label: "Wholesale", icon: Store },
+ { href: "/admin/import", label: "Import Center", icon: Upload },
+ { href: "/admin/settings/ai", label: "AI Intelligence", icon: BrainCircuit },
+ ],
+ },
+ {
+ label: "Tracking",
+ items: [
+ { href: "/admin/time-tracking", label: "Time Tracking", icon: Clock },
+ { href: "/admin/water-log", label: "Water Log", icon: Droplets, addonKey: "water_log" },
+ { href: "/admin/route-trace", label: "Route Trace", icon: Route, addonKey: "route_trace" },
+ ],
+ },
+ {
+ label: "Insights",
+ items: [
+ { href: "/admin/reports", label: "Reports", icon: ChartBar },
+ { href: "/admin/taxes", label: "Tax Dashboard", icon: Receipt },
+ ],
+ },
+ {
+ label: "Settings",
+ items: [
+ { href: "/admin/settings", label: "Settings", icon: Settings },
+ ],
+ },
];
-// Icon components
-function GridIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function CartIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function MapPinIcon({ className }: { className?: string }) {
- return (
-
-
-
-
- );
-}
-
-function PackageIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function ClipboardIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function ClockIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function MailIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function SparkleIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function SettingsCogIcon({ open }: { open: boolean }) {
- return (
-
-
-
- );
-}
-
-function ChevronIcon({ open }: { open: boolean }) {
- return (
-
-
-
- );
-}
-
-function HamburgerIcon() {
- return (
-
-
-
- );
-}
-
-function CloseIcon() {
- return (
-
-
-
- );
-}
-
-function BillingIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function PuzzleIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function PlugIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function TruckIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function SquareIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function LogoutIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function UsersIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function ShieldIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-const ICON_MAP: Record = {
- grid: ,
- "shopping-cart": ,
- "map-pin": ,
- package: ,
- clipboard: ,
- clock: ,
- mail: ,
- settings: ,
- advanced: ,
- billing: ,
- puzzle: ,
- plug: ,
- sparkles: ,
- truck: ,
- square: ,
- users: ,
- shield: ,
-};
-
type SidebarProps = {
userRole?: string | null;
brandIds?: string[];
activeBrandId?: string | null;
brands?: { id: string; name: string; slug: string; logo_url: string | null }[];
+ /**
+ * Map of add-on key → enabled. When a key is `false` (or omitted, with the
+ * add-on not in the map), any nav item with that `addonKey` is hidden.
+ * When the prop itself is omitted, all add-on-gated items are shown
+ * (back-compat with the current admin layout, which doesn't pass it).
+ */
+ enabledAddons?: Record;
};
export default function AdminSidebar({
@@ -228,6 +148,7 @@ export default function AdminSidebar({
brandIds,
activeBrandId,
brands,
+ enabledAddons,
}: SidebarProps) {
const pathname = usePathname();
const router = useRouter();
@@ -237,15 +158,52 @@ export default function AdminSidebar({
const mobileMenuRef = useRef(null);
const closeButtonRef = useRef(null);
- const roleLabel = userRole === "platform_admin" ? "Platform Admin"
- : userRole === "brand_admin" ? "Brand Admin"
- : userRole === "store_employee" ? "Store Employee"
- : null;
+ const roleLabel =
+ userRole === "platform_admin"
+ ? "Platform Admin"
+ : userRole === "brand_admin"
+ ? "Brand Admin"
+ : userRole === "store_employee"
+ ? "Store Employee"
+ : null;
- const isActive = useCallback((href: string) => {
- if (href === "/admin") return pathname === "/admin";
- return pathname.startsWith(href);
- }, [pathname]);
+ /**
+ * Build the filtered list of visible nav items for the current viewer.
+ * Honors role-gating (Command Center) and add-on gating (Water Log, Route Trace).
+ * If `enabledAddons` is undefined, add-on-gated items are shown (no gating).
+ */
+ const visibleItems: NavItemDef[] = NAV_GROUPS.flatMap((group) => {
+ return group.items.filter((item) => {
+ if (item.requiresPlatformAdmin && userRole !== "platform_admin") {
+ return false;
+ }
+ if (item.addonKey && enabledAddons && enabledAddons[item.addonKey] === false) {
+ return false;
+ }
+ return true;
+ });
+ });
+
+ /**
+ * Groups rendered, with empty ones removed. The Communications group is
+ * always rendered today (Harvest Reach is always visible) — but if a
+ * future iteration hides Harvest Reach for some role, the entire group
+ * header goes away rather than rendering an empty section.
+ */
+ const visibleGroups: NavGroup[] = NAV_GROUPS
+ .map((group) => ({
+ ...group,
+ items: group.items.filter((item) => visibleItems.includes(item)),
+ }))
+ .filter((group) => group.items.length > 0);
+
+ const isActive = useCallback(
+ (href: string) => {
+ if (href === "/admin") return pathname === "/admin";
+ return pathname.startsWith(href);
+ },
+ [pathname],
+ );
// Close mobile menu with animation
const closeMobileMenu = useCallback(() => {
@@ -283,31 +241,106 @@ export default function AdminSidebar({
};
}, [mobileOpen]);
- // Keyboard navigation for nav items
- const handleNavKeyDown = useCallback((e: KeyboardEvent, href: string, index: number) => {
- const navLinks = NAV_ITEMS.filter(item => !item.divider);
-
- if (e.key === "ArrowDown") {
- e.preventDefault();
- const nextIndex = (index + 1) % navLinks.length;
- const nextLink = document.querySelector(`[data-nav-index="${nextIndex}"]`) as HTMLAnchorElement;
- nextLink?.focus();
- } else if (e.key === "ArrowUp") {
- e.preventDefault();
- const prevIndex = index === 0 ? navLinks.length - 1 : index - 1;
- const prevLink = document.querySelector(`[data-nav-index="${prevIndex}"]`) as HTMLAnchorElement;
- prevLink?.focus();
- } else if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- router.push(href);
- if (mobileOpen) closeMobileMenu();
- }
- }, [router, mobileOpen, closeMobileMenu]);
+ // Keyboard navigation for nav items (arrow up/down loops, enter/space activates)
+ const handleNavKeyDown = useCallback(
+ (e: KeyboardEvent, href: string, index: number) => {
+ const total = visibleItems.length;
+ if (total === 0) return;
+
+ if (e.key === "ArrowDown") {
+ e.preventDefault();
+ const nextIndex = (index + 1) % total;
+ const nextLink = document.querySelector(
+ `[data-nav-index="${nextIndex}"]`,
+ ) as HTMLAnchorElement | null;
+ nextLink?.focus();
+ } else if (e.key === "ArrowUp") {
+ e.preventDefault();
+ const prevIndex = index === 0 ? total - 1 : index - 1;
+ const prevLink = document.querySelector(
+ `[data-nav-index="${prevIndex}"]`,
+ ) as HTMLAnchorElement | null;
+ prevLink?.focus();
+ } else if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ router.push(href);
+ if (mobileOpen) closeMobileMenu();
+ }
+ },
+ [router, mobileOpen, closeMobileMenu, visibleItems.length],
+ );
async function handleLogout() {
await signOutAction();
}
+ // Shared nav-link renderer. Used by both the desktop sidebar and the mobile
+ // slide-in panel — they're the same list, just in a different container.
+ const renderNavLink = (item: NavItemDef, index: number) => {
+ const active = isActive(item.href);
+ const Icon = item.icon;
+ return (
+
+ closeMobileMenu()}
+ onKeyDown={(e) => handleNavKeyDown(e, item.href, index)}
+ className={[
+ "group flex items-center gap-2.5 pl-3 pr-2.5 py-2 rounded-r-md text-xs font-medium",
+ "border-l-[3px] transition-all duration-200",
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--admin-sidebar-bg)]",
+ ].join(" ")}
+ style={
+ active
+ ? {
+ backgroundColor: "var(--admin-sidebar-active)",
+ color: "#F4F1E8",
+ borderLeftColor: "var(--admin-sidebar-accent)",
+ }
+ : {
+ color: "var(--admin-sidebar-text)",
+ borderLeftColor: "transparent",
+ }
+ }
+ onMouseEnter={(e) => {
+ if (!active) {
+ e.currentTarget.style.backgroundColor =
+ "var(--admin-sidebar-hover)";
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (!active) {
+ e.currentTarget.style.backgroundColor = "transparent";
+ }
+ }}
+ aria-current={active ? "page" : undefined}
+ tabIndex={0}
+ >
+
+
+
+ {item.label}
+ {active && (
+
+ )}
+
+
+ );
+ };
+
return (
<>
{/* Mobile hamburger button - accessible */}
@@ -319,7 +352,7 @@ export default function AdminSidebar({
aria-controls="admin-sidebar"
type="button"
>
-
+
{/* Mobile overlay backdrop with blur */}
@@ -332,27 +365,27 @@ export default function AdminSidebar({
/>
)}
- {/* Sidebar panel - Elegant warm dark with smooth transitions */}
+ {/* Sidebar panel - dark canvas, grouped nav, amber accent for active */}
>
);
-}
\ No newline at end of file
+}
diff --git a/src/components/admin/SideNavGroup.tsx b/src/components/admin/SideNavGroup.tsx
new file mode 100644
index 0000000..b2aff24
--- /dev/null
+++ b/src/components/admin/SideNavGroup.tsx
@@ -0,0 +1,49 @@
+import React from "react";
+
+/**
+ * SideNavGroup — a presentational wrapper that renders an optional group
+ * label (10px uppercase, tracking-widest) and a hairline divider, then
+ * passes the nav item children through unchanged.
+ *
+ * Used by AdminSidebar to chunk the long list of admin links into the
+ * 7 IA groups defined in `docs/superpowers/specs/2026-06-17-admin-redesign.md`
+ * (Workspace · Operations · Communications · Growth · Tracking · Insights · Settings).
+ *
+ * `collapsed` is a future-proofing prop for a per-group collapse toggle;
+ * it visually hides the children but keeps the label + divider in place.
+ */
+type SideNavGroupProps = {
+ label?: string;
+ children: React.ReactNode;
+ collapsed?: boolean;
+};
+
+export default function SideNavGroup({
+ label,
+ children,
+ collapsed = false,
+}: SideNavGroupProps) {
+ return (
+
+ {label && (
+
+
+ {label}
+
+
+ )}
+
+
+ );
+}