← back to Norma

components/Sidebar.tsx

1330 lines

'use client';

import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import ImpactStrip from '@/components/shared/ImpactStrip';
import { useOrg } from './OrgProvider';
import ManageTabsModal from './ManageTabsModal';
import {
  FileText,
  Newspaper,
  Twitter,
  BookOpen,
  Settings,
  Megaphone,
  Award,
  Plug,
  ChevronLeft,
  ChevronRight,
  ChevronDown,
  GraduationCap,
  Trophy,
  Handshake,
  Users,
  Flame,
  Send,
  BookOpenCheck,
  MessageCircle,
  MapPin,
  AlertTriangle,
  Target,
  BarChart3,
  TrendingUp,
  FileInput,
  Lightbulb,
  Network,
  GitFork,
  Building2,
  Landmark,
  Vote,
  Waypoints,
  Bot,
  ScrollText,
  Search,
  UserPlus,
  Radio,
  Rocket,
  GitBranch,
  Map,
  Grid3x3,
  Crosshair,
  Globe,
  Database,
  DollarSign,
  BadgeDollarSign,
  Key,
  Zap,
  Brain,
  Activity,
  Rss,
  LayoutDashboard,
  Boxes,
  Sun,
  MoreHorizontal,
  Globe2,
  Mail,
  Inbox,
  Sparkles,
  Eye,
  EyeOff,
  SlidersHorizontal,
  Clock,
  FolderClock,
} from 'lucide-react';

/* ─── Types ──────────────────────────────────────────────────────────────── */
export type TabId =
  | 'dashboard'
  | 'drafts'
  | 'news'
  | 'petitions'
  | 'petition-ideas'
  | 'grants'
  | 'funding'
  | 'collaborations'
  | 'partners'
  | 'oldleads'
  | 'heypartner'
  | 'playbook'
  | 'community'
  | 'xsessions'
  | 'library'
  | 'integrations'
  | 'awards'
  | 'settings'
  | 'idea-mindmap'
  | 'partner-tree'
  | 'organizations'
  | 'foundations'
  | 'geo-report'
  | 'geo-hotspots'
  | 'geo-advocacy'
  | 'geo-scatter'
  | 'geo-trends'
  | 'geo-moveon'
  | 'pulse-feed'
  | 'congress'
  | 'opportunity-map'
  | 'agent-hub'
  | 'agent-petition'
  | 'agent-grants'
  | 'agent-advocacy'
  | 'agent-onboard'
  | 'moveon-launch'
  | 'pipeline'
  | 'geo-choropleth'
  | 'geo-topic-grid'
  | 'topic-radar'
  | 'statements'
  | 'pulse-globe'
  | 'pulse-mindmap'
  | 'pulse-datasources'
  | 'pulse-onboard'
  | 'petition-creator'
  | 'trending-petitions'
  | 'journalists'
  | 'nexus-explorer'
  | 'nexus-power-map'
  | 'nexus-opportunities'
  | 'nexus-deep-insights'
  | 'pulse-heatmap'
  | 'live-news'
  | 'viz-hub'
  | 'viz-tree'
  | 'viz-matrix'
  | 'viz-3d'
  | 'viz-sunburst'
  | 'apis'
  | 'donations'
  | 'contacts'
  | 'org-connections'
  | 'power-intelligence'
  | 'in-the-news'
  | 'email-sends'
  | 'email-statements'
  | 'email-donations'
  | 'email-breaking'
  | 'email-education'
  | 'email-workshops'
  | 'email-congress'
  | 'email-agencies'
  | 'email-journalists'
  | 'clients'
  | 'gmail-crm'
  | 'email-analyzer'
  | 'email-analyzer-saved'
  | 'price-tracker'
  | 'social-overview'
  | 'social-compose'
  | 'social-schedule'
  | 'social-analytics'
  | 'social-accounts'
  | 'social-competitors'
  | 'social-scoreboard';

type SectionId =
  | 'workspace'
  | 'email-sends'
  | 'social'
  | 'intelligence'
  | 'geo'
  | 'pulse'
  | 'congress'
  | 'nexus'
  | 'agents'
  | 'universal';

interface NavItem {
  id: TabId;
  label: string;
  Icon: React.ElementType;
  section: SectionId;
}

interface SidebarProps {
  activeTab: TabId;
  onTabChange: (tab: TabId) => void;
  isOpen?: boolean;
  onToggle?: () => void;
  role?: 'admin' | 'staff' | 'intern' | 'pulse';
}

/* ─── Tabs excluded for staff role ────────────────────────────────────────── */
/* Staff only sees Email & Outreach + Settings by default.
   Other modules can be activated from Settings → Module Manager. */
const STAFF_DEFAULT_TABS: TabId[] = [
  // Workspace
  'dashboard', 'drafts', 'news', 'petitions', 'grants', 'pipeline', 'statements', 'donations',
  // Email & Outreach
  'email-sends', 'email-analyzer', 'email-analyzer-saved', 'gmail-crm', 'clients',
  // Intelligence
  'power-intelligence', 'in-the-news', 'journalists', 'contacts', 'community',
  'topic-radar', 'petition-creator', 'trending-petitions', 'live-news',
  // Congress
  'congress',
];
const STAFF_EXCLUDED_TABS: TabId[] = [
  'apis',
  'integrations',
  'organizations',
  'foundations',
  'org-connections',
];

/* ─── Section config ─────────────────────────────────────────────────────── */
const ORG_LOCAL_SECTIONS: SectionId[] = [
  'workspace', 'email-sends', 'social', 'intelligence', 'geo', 'pulse', 'congress', 'nexus', 'agents',
];

interface SectionMeta {
  label: (orgShortName?: string) => string;
  defaultOpen: boolean;
}

const SECTION_META: Record<SectionId, SectionMeta> = {
  workspace:    { label: (s) => s ? `${s} Workspace` : 'Workspace', defaultOpen: true  },
  'email-sends': { label: () => 'Email & Outreach',                   defaultOpen: false },
  social:       { label: () => 'Social Media',                       defaultOpen: false },
  intelligence: { label: () => 'Intelligence',                       defaultOpen: false },
  geo:          { label: () => 'Geo Intelligence',                   defaultOpen: false },
  pulse:        { label: () => 'Pulse',                              defaultOpen: false },
  congress:     { label: () => 'Congress',                           defaultOpen: false },
  nexus:        { label: () => 'Nexus',                              defaultOpen: false },
  agents:       { label: () => 'AI Agents',                          defaultOpen: false },
  universal:    { label: () => 'Universal',                          defaultOpen: false },
};

/* ─── Keyboard shortcut hints ────────────────────────────────────────────── */
const KBD_MAP: Partial<Record<TabId, string>> = {
  dashboard:     'H',
  drafts:        'D',
  petitions:     'P',
  'topic-radar': 'R',
  pipeline:      'L',
  contacts:      'N',
};

/* ─── Navigation config ──────────────────────────────────────────────────── */
const NAV_ITEMS: NavItem[] = [
  // workspace — org-local, pinned at top
  { id: 'dashboard',           label: 'Dashboard',          Icon: LayoutDashboard, section: 'workspace'    },
  { id: 'drafts',              label: 'Drafts',             Icon: FileText,        section: 'workspace'    },
  { id: 'news',                label: 'News',               Icon: Newspaper,       section: 'workspace'    },
  { id: 'petitions',           label: 'Petitions',          Icon: Megaphone,       section: 'workspace'    },
  { id: 'grants',              label: 'Grants',             Icon: Award,           section: 'workspace'    },
  { id: 'pipeline',            label: 'Pipeline',           Icon: GitBranch,       section: 'workspace'    },
  { id: 'statements',          label: 'Statements',         Icon: Megaphone,       section: 'workspace'    },
  { id: 'donations',           label: 'Donations',          Icon: DollarSign,      section: 'workspace'    },

  // email-sends — org-local (type filter is a dropdown inside Email Center)
  { id: 'email-sends',         label: 'Email Center',       Icon: Mail,            section: 'email-sends'  },
  { id: 'email-analyzer',      label: 'Email Analyzer',     Icon: Sparkles,        section: 'email-sends'  },
  { id: 'email-analyzer-saved', label: 'Saved Analyses',    Icon: FolderClock,     section: 'email-sends'  },
  { id: 'clients',             label: 'Clients / Borrowers', Icon: Users,          section: 'email-sends'  },
  { id: 'gmail-crm',           label: 'Gmail CRM',          Icon: Inbox,           section: 'email-sends'  },

  // social — org-local
  { id: 'social-overview',     label: 'Social Hub',         Icon: Globe2,          section: 'social'       },
  { id: 'social-compose',      label: 'Compose',            Icon: Send,            section: 'social'       },
  { id: 'social-schedule',     label: 'Schedule',           Icon: Clock,           section: 'social'       },
  { id: 'social-analytics',    label: 'Analytics',          Icon: BarChart3,       section: 'social'       },
  { id: 'social-competitors',  label: 'Competitors',        Icon: Eye,             section: 'social'       },
  { id: 'social-scoreboard',   label: 'Scoreboard',         Icon: Trophy,          section: 'social'       },
  { id: 'social-accounts',     label: 'Accounts',           Icon: Key,             section: 'social'       },

  // intelligence — org-local
  { id: 'power-intelligence',  label: 'Power Intel',        Icon: Zap,             section: 'intelligence' },
  { id: 'in-the-news',         label: 'In the News',        Icon: Rss,             section: 'intelligence' },
  { id: 'journalists',         label: 'Journalists',        Icon: Users,           section: 'intelligence' },
  { id: 'contacts',            label: 'Network',            Icon: UserPlus,        section: 'intelligence' },
  { id: 'community',           label: 'Community',          Icon: MessageCircle,   section: 'intelligence' },
  { id: 'topic-radar',         label: 'Topic Radar',        Icon: Crosshair,       section: 'intelligence' },
  { id: 'petition-ideas',      label: 'Petition Ideas',     Icon: Lightbulb,       section: 'intelligence' },
  { id: 'petition-creator',    label: 'Create Petition',    Icon: Rocket,          section: 'intelligence' },
  { id: 'trending-petitions',  label: 'Trending Petitions', Icon: TrendingUp,      section: 'intelligence' },
  { id: 'live-news',           label: 'Live News',          Icon: Rss,             section: 'intelligence' },
  { id: 'price-tracker',       label: 'Price Tracker',      Icon: BadgeDollarSign, section: 'intelligence' },

  // geo — org-local
  { id: 'geo-report',          label: 'Zip Report',         Icon: MapPin,          section: 'geo'          },
  { id: 'geo-hotspots',        label: 'Hotspot Map',        Icon: AlertTriangle,   section: 'geo'          },
  { id: 'geo-advocacy',        label: 'Advocacy Targets',   Icon: Target,          section: 'geo'          },
  { id: 'geo-scatter',         label: 'Cross-Ref',          Icon: BarChart3,       section: 'geo'          },
  { id: 'geo-trends',          label: 'Trend Pulse',        Icon: TrendingUp,      section: 'geo'          },
  { id: 'geo-moveon',          label: 'MoveOn Import',      Icon: FileInput,       section: 'geo'          },
  { id: 'moveon-launch',       label: 'MoveOn Launch',      Icon: Rocket,          section: 'geo'          },
  { id: 'geo-choropleth',      label: 'State Heat Map',     Icon: Map,             section: 'geo'          },
  { id: 'geo-topic-grid',      label: 'Topic Grid',         Icon: Grid3x3,         section: 'geo'          },

  // pulse — org-local
  { id: 'pulse-feed',          label: 'Pulse Feed',         Icon: Radio,           section: 'pulse'        },
  { id: 'pulse-globe',         label: 'Globe',              Icon: Globe,           section: 'pulse'        },
  { id: 'pulse-mindmap',       label: 'Mind Map',           Icon: Network,         section: 'pulse'        },
  { id: 'pulse-datasources',   label: 'Data Sources',       Icon: Database,        section: 'pulse'        },
  { id: 'pulse-onboard',       label: 'OnBoard',            Icon: UserPlus,        section: 'pulse'        },
  { id: 'pulse-heatmap',       label: 'Pulse Heat Map',     Icon: Activity,        section: 'pulse'        },

  // congress — org-local
  { id: 'congress',            label: 'Congress',           Icon: Vote,            section: 'congress'     },
  { id: 'opportunity-map',     label: 'Opportunity Map',    Icon: Waypoints,       section: 'congress'     },

  // nexus — org-local
  { id: 'nexus-explorer',      label: 'Nexus Graph',        Icon: Network,         section: 'nexus'        },
  { id: 'nexus-power-map',     label: 'Power Map',          Icon: Zap,             section: 'nexus'        },
  { id: 'nexus-opportunities', label: 'Opportunities',      Icon: Target,          section: 'nexus'        },
  { id: 'nexus-deep-insights', label: 'Deep Insights',      Icon: Brain,           section: 'nexus'        },
  { id: 'viz-hub',             label: 'Viz Hub',            Icon: LayoutDashboard, section: 'nexus'        },
  { id: 'viz-tree',            label: 'Connection Tree',    Icon: GitFork,         section: 'nexus'        },
  { id: 'viz-matrix',          label: 'Correlation Map',    Icon: Grid3x3,         section: 'nexus'        },
  { id: 'viz-3d',              label: '3D Network',         Icon: Boxes,           section: 'nexus'        },
  { id: 'viz-sunburst',        label: 'Sunburst',           Icon: Sun,             section: 'nexus'        },

  // agents — org-local
  { id: 'agent-hub',           label: 'Agent Apps',         Icon: Bot,             section: 'agents'       },
  { id: 'agent-petition',      label: 'Petition Builder',   Icon: ScrollText,      section: 'agents'       },
  { id: 'agent-grants',        label: 'Grant Finder',       Icon: Search,          section: 'agents'       },
  { id: 'agent-advocacy',      label: 'Advocacy Kit',       Icon: Target,          section: 'agents'       },
  { id: 'agent-onboard',       label: 'Org Onboard',        Icon: UserPlus,        section: 'agents'       },

  // universal — same for all orgs
  { id: 'organizations',       label: 'Organizations',      Icon: Building2,       section: 'universal'    },
  { id: 'foundations',         label: 'Foundations',        Icon: Landmark,        section: 'universal'    },
  { id: 'library',             label: 'Library',            Icon: BookOpen,        section: 'universal'    },
  { id: 'apis',                label: 'API Keys',           Icon: Key,             section: 'universal'    },
  { id: 'integrations',        label: 'Integrations',       Icon: Plug,            section: 'universal'    },
  { id: 'awards',              label: 'Awards',             Icon: Trophy,          section: 'universal'    },
  { id: 'org-connections',     label: 'Org Connections',    Icon: Network,         section: 'universal'    },
  { id: 'settings',            label: 'Settings',           Icon: Settings,        section: 'universal'    },
  { id: 'xsessions',           label: 'X Sessions',         Icon: Twitter,         section: 'universal'    },
  { id: 'collaborations',      label: 'Collaborations',     Icon: Handshake,       section: 'universal'    },
  { id: 'partners',            label: 'Partners',           Icon: Users,           section: 'universal'    },
  { id: 'oldleads',            label: 'Old Leads',          Icon: Flame,           section: 'universal'    },
  { id: 'heypartner',          label: 'Hey Partner',        Icon: Send,            section: 'universal'    },
  { id: 'playbook',            label: 'Playbook',           Icon: BookOpenCheck,   section: 'universal'    },
  { id: 'funding',             label: 'Funding',            Icon: GraduationCap,   section: 'universal'    },
  { id: 'idea-mindmap',        label: 'Idea Mind Map',      Icon: Brain,           section: 'universal'    },
  { id: 'partner-tree',        label: 'Partner Tree',       Icon: GitFork,         section: 'universal'    },
];

/* ─── Pinned workspace items (always visible before "More") ──────────────── */
const PINNED_IDS: TabId[] = [
  'dashboard', 'drafts', 'news', 'petitions', 'grants', 'pipeline', 'contacts', 'community',
];

/* ─── localStorage helpers ───────────────────────────────────────────────── */
function readLS(key: string, fallback: boolean): boolean {
  if (typeof window === 'undefined') return fallback;
  try {
    const v = localStorage.getItem(key);
    return v === null ? fallback : v === 'true';
  } catch {
    return fallback;
  }
}

function writeLS(key: string, value: boolean) {
  try { localStorage.setItem(key, String(value)); } catch { /* noop */ }
}

/* ─── Hidden tabs helpers ───────────────────────────────────────────────── */

function getHiddenTabsKey(role?: 'admin' | 'staff' | 'intern' | 'pulse'): string {
  if (role === 'staff' || role === 'intern' || role === 'pulse') {
    const orgId = typeof window !== 'undefined'
      ? localStorage.getItem('norma-user-orgid') || 'default'
      : 'default';
    return `nikki-hidden-tabs-${orgId}`;
  }
  return 'norma-hidden-tabs';
}

function readHiddenTabs(key: string): Set<TabId> {
  if (typeof window === 'undefined') return new Set();
  try {
    const raw = localStorage.getItem(key);
    if (!raw) return new Set();
    const arr = JSON.parse(raw);
    if (Array.isArray(arr)) return new Set(arr as TabId[]);
  } catch { /* noop */ }
  return new Set();
}

function writeHiddenTabs(key: string, tabs: Set<TabId>) {
  try {
    localStorage.setItem(key, JSON.stringify(Array.from(tabs)));
  } catch { /* noop */ }
}

/* ─── OrgBadge ───────────────────────────────────────────────────────────── */
function OrgBadge({ shortName, brandColor }: { shortName: string; brandColor: string }) {
  return (
    <span
      aria-label={`Org: ${shortName}`}
      style={{
        display: 'inline-flex',
        alignItems: 'center',
        padding: '1px 6px',
        borderRadius: 4,
        fontSize: 9,
        fontWeight: 700,
        letterSpacing: '0.04em',
        textTransform: 'uppercase',
        background: `${brandColor}22`,
        color: brandColor,
        border: `1px solid ${brandColor}44`,
        lineHeight: 1.5,
        flexShrink: 0,
      }}
    >
      {shortName.slice(0, 4)}
    </span>
  );
}

/* ─── UniversalDivider ───────────────────────────────────────────────────── */
function UniversalDivider({ collapsed }: { collapsed: boolean }) {
  if (collapsed) {
    return (
      <div
        aria-hidden="true"
        style={{
          height: 1,
          margin: '10px 12px',
          background: 'linear-gradient(90deg, transparent, var(--color-border), transparent)',
          borderRadius: 1,
        }}
      />
    );
  }

  return (
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        gap: 8,
        margin: '14px 8px 2px',
      }}
      aria-label="Universal section — same across all organizations"
    >
      <div
        style={{
          flex: 1,
          height: 1,
          background: 'var(--color-border)',
          borderRadius: 1,
        }}
        aria-hidden="true"
      />
      <div
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: 4,
          padding: '2px 8px',
          borderRadius: 20,
          border: '1px solid var(--color-border)',
          backgroundColor: 'var(--color-surface-el)',
        }}
      >
        <Globe2 size={10} aria-hidden="true" style={{ color: 'var(--color-text-muted)' }} />
        <span
          style={{
            fontSize: 9,
            fontWeight: 700,
            letterSpacing: '0.1em',
            textTransform: 'uppercase',
            color: 'var(--color-text-muted)',
          }}
        >
          Universal
        </span>
      </div>
      <div
        style={{
          flex: 1,
          height: 1,
          background: 'var(--color-border)',
          borderRadius: 1,
        }}
        aria-hidden="true"
      />
    </div>
  );
}

/* ─── SidebarSection ─────────────────────────────────────────────────────── */
interface SidebarSectionProps {
  sectionId: SectionId;
  title: string;
  items: NavItem[];
  collapsed: boolean;
  activeTab: TabId;
  onTabChange: (tab: TabId) => void;
  defaultOpen?: boolean;
  pinnedCount?: number;
  orgBadge?: { shortName: string; brandColor: string } | null;
  onToggleHide?: (tabId: TabId) => void;
  hiddenTabs?: Set<TabId>;
}

function SidebarSection({
  sectionId,
  title,
  items,
  collapsed,
  activeTab,
  onTabChange,
  defaultOpen = false,
  pinnedCount,
  orgBadge,
  onToggleHide,
  hiddenTabs,
}: SidebarSectionProps) {
  const lsKey = `sidebar-section-${sectionId}`;
  const activeIsHere = items.some((i) => i.id === activeTab);

  const [isOpen, setIsOpen] = useState<boolean>(() =>
    activeIsHere ? true : readLS(lsKey, defaultOpen)
  );

  const [moreOpen, setMoreOpen] = useState<boolean>(() =>
    readLS(`${lsKey}-more`, false)
  );

  const prevActiveIsHere = useRef(activeIsHere);
  useEffect(() => {
    // Only auto-expand when the active tab enters this section from outside,
    // not when the user manually collapses it while already in the section.
    if (activeIsHere && !prevActiveIsHere.current && !isOpen) {
      setIsOpen(true);
      writeLS(lsKey, true);
    }
    prevActiveIsHere.current = activeIsHere;
  }, [activeIsHere, isOpen, lsKey]);

  const pinnedItems   = pinnedCount !== undefined ? items.slice(0, pinnedCount) : items;
  const overflowItems = pinnedCount !== undefined ? items.slice(pinnedCount)    : [];
  const activeIsInOverflow = overflowItems.some((i) => i.id === activeTab);

  useEffect(() => {
    if (activeIsInOverflow && !moreOpen) {
      setMoreOpen(true);
      writeLS(`${lsKey}-more`, true);
    }
  }, [activeIsInOverflow, moreOpen, lsKey]);

  const toggleSection = useCallback(() => {
    setIsOpen((prev) => { writeLS(lsKey, !prev); return !prev; });
  }, [lsKey]);

  const toggleMore = useCallback(() => {
    setMoreOpen((prev) => { writeLS(`${lsKey}-more`, !prev); return !prev; });
  }, [lsKey]);

  // Icon-only (collapsed) mode: dot divider + all items
  if (collapsed) {
    return (
      <>
        <div
          aria-hidden="true"
          style={{
            height: 1,
            margin: '6px 12px',
            background: 'var(--color-border)',
            borderRadius: 1,
            opacity: 0.5,
          }}
        />
        <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
          {items.map(({ id, label, Icon }) => (
            <SidebarButton
              key={id}
              id={id}
              label={label}
              Icon={Icon}
              isActive={activeTab === id}
              collapsed={collapsed}
              onClick={() => onTabChange(id)}
              kbd={KBD_MAP[id]}
              onToggleHide={onToggleHide}
              isHiddenByUser={hiddenTabs?.has(id)}
            />
          ))}
        </div>
      </>
    );
  }

  return (
    <div>
      {/* Section header */}
      <button
        type="button"
        onClick={toggleSection}
        aria-expanded={isOpen}
        style={{
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
          width: '100%',
          padding: '8px 8px',
          minHeight: 36,
          marginTop: 8,
          background: 'none',
          border: 'none',
          cursor: 'pointer',
          borderRadius: 6,
          color: 'var(--color-text-muted)',
          fontSize: 11,
          fontWeight: 700,
          letterSpacing: '0.06em',
          textTransform: 'uppercase',
        }}
        onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--color-text-secondary)'; }}
        onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--color-text-muted)'; }}
      >
        {/* Title + optional org badge */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, overflow: 'hidden' }}>
          <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
            {title}
          </span>
          {orgBadge && (
            <OrgBadge shortName={orgBadge.shortName} brandColor={orgBadge.brandColor} />
          )}
        </div>

        {/* Count + chevron */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 4, flexShrink: 0 }}>
          {!isOpen && (
            <span style={{ fontSize: 9, color: 'var(--color-text-muted)', opacity: 0.6 }}>
              {items.length}
            </span>
          )}
          <ChevronDown
            size={14}
            aria-hidden="true"
            style={{
              transform: isOpen ? 'rotate(0deg)' : 'rotate(-90deg)',
              transition: 'transform 0.2s',
            }}
          />
        </div>
      </button>

      {/* Items */}
      {isOpen && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
          {pinnedItems.map(({ id, label, Icon }) => (
            <SidebarButton
              key={id}
              id={id}
              label={label}
              Icon={Icon}
              isActive={activeTab === id}
              collapsed={collapsed}
              onClick={() => onTabChange(id)}
              kbd={KBD_MAP[id]}
              onToggleHide={onToggleHide}
              isHiddenByUser={hiddenTabs?.has(id)}
            />
          ))}

          {/* Overflow "More" area */}
          {overflowItems.length > 0 && (
            <>
              <button
                type="button"
                onClick={toggleMore}
                aria-expanded={moreOpen}
                style={{
                  display: 'flex',
                  alignItems: 'center',
                  gap: 6,
                  padding: '8px 12px',
                  minHeight: 38,
                  border: 'none',
                  background: 'none',
                  cursor: 'pointer',
                  color: 'var(--color-text-muted)',
                  fontSize: 13,
                  fontWeight: 500,
                  borderRadius: 6,
                  width: '100%',
                  textAlign: 'left',
                }}
                onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--color-text-secondary)'; }}
                onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--color-text-muted)'; }}
              >
                <MoreHorizontal size={13} aria-hidden="true" />
                <span>{moreOpen ? 'Less' : `${overflowItems.length} more`}</span>
                <ChevronDown
                  size={11}
                  aria-hidden="true"
                  style={{
                    transform: moreOpen ? 'rotate(0deg)' : 'rotate(-90deg)',
                    transition: 'transform 0.2s',
                    marginLeft: 'auto',
                  }}
                />
              </button>

              {moreOpen && overflowItems.map(({ id, label, Icon }) => (
                <SidebarButton
                  key={id}
                  id={id}
                  label={label}
                  Icon={Icon}
                  isActive={activeTab === id}
                  collapsed={collapsed}
                  onClick={() => onTabChange(id)}
                  kbd={KBD_MAP[id]}
                  onToggleHide={onToggleHide}
                  isHiddenByUser={hiddenTabs?.has(id)}
                />
              ))}
            </>
          )}
        </div>
      )}
    </div>
  );
}

/* ─── Sidebar ────────────────────────────────────────────────────────────── */
export default function Sidebar({ activeTab, onTabChange, isOpen, onToggle, role }: SidebarProps) {
  const [collapsed, setCollapsed] = useState(false);
  const [orgDropdownOpen, setOrgDropdownOpen] = useState(false);
  const [manageTabsOpen, setManageTabsOpen] = useState(false);
  const { org, orgs, switchOrg } = useOrg();

  const brandColor = org?.brand_color || '#10b981';
  const brandShort = org?.short_name   || 'N';
  const isAdmin = !role || role === 'admin';

  // ── Hidden tabs persistence ───────────────────────────────────────────
  const hiddenTabsKey = useMemo(() => getHiddenTabsKey(role), [role]);
  const [hiddenTabs, setHiddenTabs] = useState<Set<TabId>>(() =>
    readHiddenTabs(hiddenTabsKey),
  );

  // Re-read hidden tabs when the key changes (role or org switch)
  useEffect(() => {
    setHiddenTabs(readHiddenTabs(hiddenTabsKey));
  }, [hiddenTabsKey]);

  const toggleHideTab = useCallback((tabId: TabId) => {
    setHiddenTabs((prev) => {
      const next = new Set(prev);
      if (next.has(tabId)) next.delete(tabId);
      else next.add(tabId);
      writeHiddenTabs(hiddenTabsKey, next);
      return next;
    });
  }, [hiddenTabsKey]);

  const resetHiddenTabs = useCallback(() => {
    setHiddenTabs(new Set());
    writeHiddenTabs(hiddenTabsKey, new Set());
  }, [hiddenTabsKey]);

  // Staff: only show default tabs + any user-activated modules from localStorage
  const [staffActivatedModules, setStaffActivatedModules] = useState<Set<string>>(new Set());

  useEffect(() => {
    if (role !== 'staff') return;
    function loadModules() {
      try {
        const saved = localStorage.getItem('nikki-activated-modules');
        if (saved) setStaffActivatedModules(new Set(JSON.parse(saved)));
        else setStaffActivatedModules(new Set());
      } catch {}
    }
    loadModules();
    // Listen for changes from ModuleManager
    window.addEventListener('storage', loadModules);
    return () => window.removeEventListener('storage', loadModules);
  }, [role]);

  const roleFilteredItems = role === 'staff'
    ? NAV_ITEMS.filter((i) =>
        STAFF_DEFAULT_TABS.includes(i.id) ||
        staffActivatedModules.has(i.id)
      ).filter((i) => !STAFF_EXCLUDED_TABS.includes(i.id))
    : NAV_ITEMS;

  // Filter out user-hidden tabs (but never hide the active tab)
  const filteredNavItems = roleFilteredItems.filter(
    (i) => !hiddenTabs.has(i.id) || i.id === activeTab,
  );

  // All tabs available for this role (used by ManageTabsModal)
  const allAvailableTabs = useMemo(
    () => roleFilteredItems.map((i) => ({ id: i.id, label: i.label, section: i.section })),
    [roleFilteredItems],
  );

  // Group items by section
  const itemsBySection = (section: SectionId) =>
    filteredNavItems.filter((i) => i.section === section);

  // Workspace uses PINNED_IDS to split pinned vs overflow;
  // pinned items that appear in workspace come first up to PINNED_IDS.length
  const workspaceItems  = itemsBySection('workspace');
  // Build pinned-first order: items in PINNED_IDS order, then the rest (using filtered list)
  const workspacePinned = PINNED_IDS.filter((id) => workspaceItems.find((i) => i.id === id));
  const workspaceRest   = workspaceItems.filter((i) => !PINNED_IDS.includes(i.id));
  const workspaceOrdered = [
    ...workspacePinned.map((id) => workspaceItems.find((i) => i.id === id)!),
    ...workspaceRest,
  ];

  // Org badge props for org-local section headers
  const orgBadge = { shortName: brandShort, brandColor };

  return (
    <>
      {isOpen && onToggle && (
        <div
          onClick={onToggle}
          className="sidebar-backdrop"
          style={{
            position: 'fixed',
            inset: 0,
            backgroundColor: 'rgba(0,0,0,0.25)',
            zIndex: 39,
          }}
        />
      )}
      <aside
        className={`sidebar-root${isOpen ? ' sidebar-open' : ''}`}
        style={{
          width: collapsed ? 64 : 240,
          minWidth: collapsed ? 64 : 240,
          transition: 'width 0.2s cubic-bezier(0.4, 0, 0.2, 1), min-width 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
          height: '100vh',
          display: 'flex',
          flexDirection: 'column',
          backgroundColor: 'var(--color-surface)',
          borderRight: '1px solid var(--color-border)',
          position: 'relative',
          overflow: 'hidden',
          flexShrink: 0,
        }}
        aria-label="Main navigation"
      >
        {/* ── Org Switcher ─────────────────────────────────────────────────── */}
        <div
          style={{
            padding: collapsed ? '12px 0' : '10px 12px',
            borderBottom: '1px solid var(--color-border)',
            flexShrink: 0,
            position: 'relative',
          }}
        >
          <button
            type="button"
            onClick={() => isAdmin && !collapsed && setOrgDropdownOpen((p) => !p)}
            title={collapsed ? (org?.org_name || 'Norma') : undefined}
            style={{
              display: 'flex',
              alignItems: 'center',
              gap: 10,
              width: '100%',
              padding: collapsed ? '4px 0' : '6px 8px',
              justifyContent: collapsed ? 'center' : 'flex-start',
              border: 'none',
              background: orgDropdownOpen ? 'var(--color-surface-el)' : 'none',
              borderRadius: 8,
              cursor: isAdmin && orgs.length > 1 ? 'pointer' : 'default',
              transition: 'background-color 0.15s',
            }}
            onMouseEnter={(e) => { if (isAdmin && orgs.length > 1) e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
            onMouseLeave={(e) => { if (!orgDropdownOpen) e.currentTarget.style.backgroundColor = 'transparent'; }}
          >
            <div
              style={{
                width: 32,
                height: 32,
                borderRadius: 10,
                background: `linear-gradient(135deg, ${brandColor}, ${brandColor}cc)`,
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                flexShrink: 0,
                boxShadow: `0 2px 8px ${brandColor}4d`,
              }}
            >
              <span style={{ color: '#fff', fontSize: brandShort.length > 3 ? 10 : 14, fontWeight: 900, lineHeight: 1 }}>
                {brandShort.slice(0, 4)}
              </span>
            </div>
            {!collapsed && (
              <div style={{ overflow: 'hidden', whiteSpace: 'nowrap', flex: 1, textAlign: 'left' }}>
                <div style={{ fontSize: 14, fontWeight: 700, color: 'var(--color-text)', lineHeight: 1.2, fontFamily: 'var(--font-heading)', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                  {org?.org_name || 'Norma'}
                </div>
                <div style={{ fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.2, letterSpacing: '0.02em' }}>
                  Nonprofit Hub
                </div>
              </div>
            )}
            {!collapsed && isAdmin && orgs.length > 1 && (
              <ChevronDown
                size={13}
                style={{
                  color: 'var(--color-text-muted)',
                  flexShrink: 0,
                  transform: orgDropdownOpen ? 'rotate(180deg)' : 'rotate(0deg)',
                  transition: 'transform 0.2s',
                }}
              />
            )}
          </button>

          {/* Org dropdown — admin only */}
          {isAdmin && orgDropdownOpen && !collapsed && (
            <>
              <div
                style={{ position: 'fixed', inset: 0, zIndex: 99 }}
                onClick={() => setOrgDropdownOpen(false)}
              />
              <div
                style={{
                  position: 'absolute',
                  top: '100%',
                  left: 8,
                  right: 8,
                  marginTop: 4,
                  zIndex: 100,
                  backgroundColor: 'var(--color-surface)',
                  border: '1px solid var(--color-border)',
                  borderRadius: 10,
                  boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
                  padding: 6,
                  maxHeight: 280,
                  overflowY: 'auto',
                }}
              >
                <div style={{ padding: '4px 8px 6px', fontSize: 10, fontWeight: 600, color: 'var(--color-text-muted)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
                  Switch Organization
                </div>
                {orgs.map((o) => (
                  <button
                    key={o.id}
                    type="button"
                    onClick={() => {
                      switchOrg(o.id);
                      setOrgDropdownOpen(false);
                    }}
                    style={{
                      display: 'flex',
                      alignItems: 'center',
                      gap: 10,
                      width: '100%',
                      padding: '8px 10px',
                      border: 'none',
                      borderRadius: 8,
                      cursor: 'pointer',
                      background: o.id === org?.id ? `${o.brand_color}22` : 'none',
                      borderLeft: o.id === org?.id ? `3px solid ${o.brand_color}` : '3px solid transparent',
                      transition: 'background-color 0.15s',
                    }}
                    onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
                    onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = o.id === org?.id ? `${o.brand_color}22` : 'transparent'; }}
                  >
                    <div
                      style={{
                        width: 26,
                        height: 26,
                        borderRadius: 8,
                        background: `linear-gradient(135deg, ${o.brand_color}, ${o.brand_color}cc)`,
                        display: 'flex',
                        alignItems: 'center',
                        justifyContent: 'center',
                        flexShrink: 0,
                      }}
                    >
                      <span style={{ color: '#fff', fontSize: (o.short_name || '').length > 3 ? 8 : 11, fontWeight: 800 }}>
                        {(o.short_name || o.org_name.slice(0, 2)).slice(0, 4)}
                      </span>
                    </div>
                    <div style={{ textAlign: 'left', overflow: 'hidden' }}>
                      <div style={{
                        fontSize: 14,
                        fontWeight: o.id === org?.id ? 600 : 500,
                        color: 'var(--color-text)',
                        overflow: 'hidden',
                        textOverflow: 'ellipsis',
                        whiteSpace: 'nowrap',
                        lineHeight: 1.2,
                      }}>
                        {o.org_name}
                      </div>
                      <div style={{ fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.2 }}>
                        {o.address_city}{o.address_city && o.address_state ? ', ' : ''}{o.address_state}
                      </div>
                    </div>
                  </button>
                ))}
              </div>
            </>
          )}
        </div>

        {/* ── Main Nav ─────────────────────────────────────────────────────── */}
        <nav
          style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: '4px 8px 0' }}
          aria-label="Primary navigation"
        >
          {/* ── ORG-LOCAL sections ──────────────────────────────────────────── */}

          {/* Workspace — pinned at top with overflow "More" */}
          <SidebarSection
            sectionId="workspace"
            title={SECTION_META.workspace.label(collapsed ? undefined : brandShort)}
            items={workspaceOrdered}
            collapsed={collapsed}
            activeTab={activeTab}
            onTabChange={onTabChange}
            defaultOpen={SECTION_META.workspace.defaultOpen}
            pinnedCount={workspacePinned.length}
            orgBadge={collapsed ? null : orgBadge}
            onToggleHide={toggleHideTab}
            hiddenTabs={hiddenTabs}
          />

          {/* Org-local sections — only render if they have items */}
          {([
            { sectionId: 'email-sends' as SectionId, orgBadged: true },
            { sectionId: 'social' as SectionId, orgBadged: false },
            { sectionId: 'intelligence' as SectionId, orgBadged: true },
            { sectionId: 'geo' as SectionId, orgBadged: true },
            { sectionId: 'pulse' as SectionId, orgBadged: true },
            { sectionId: 'congress' as SectionId, orgBadged: true },
            { sectionId: 'nexus' as SectionId, orgBadged: true },
            { sectionId: 'agents' as SectionId, orgBadged: true },
          ] as const).map(({ sectionId, orgBadged }) => {
            const items = itemsBySection(sectionId);
            if (items.length === 0) return null;
            return (
              <SidebarSection
                key={sectionId}
                sectionId={sectionId}
                title={SECTION_META[sectionId].label()}
                items={items}
                collapsed={collapsed}
                activeTab={activeTab}
                onTabChange={onTabChange}
                defaultOpen={SECTION_META[sectionId].defaultOpen}
                orgBadge={orgBadged && !collapsed ? orgBadge : undefined}
                onToggleHide={toggleHideTab}
                hiddenTabs={hiddenTabs}
              />
            );
          })}

          {/* ── UNIVERSAL divider + section — only if it has items ──────────── */}
          {itemsBySection('universal').length > 0 && (
            <>
              <UniversalDivider collapsed={collapsed} />
              <SidebarSection
                sectionId="universal"
                title={SECTION_META.universal.label()}
                items={itemsBySection('universal')}
                collapsed={collapsed}
                activeTab={activeTab}
                onTabChange={onTabChange}
                defaultOpen={SECTION_META.universal.defaultOpen}
                onToggleHide={toggleHideTab}
                hiddenTabs={hiddenTabs}
              />
            </>
          )}

          {/* Bottom spacer */}
          <div style={{ height: 8 }} />
        </nav>

        {/* ── Impact Strip ─────────────────────────────────────────────────── */}
        <ImpactStrip collapsed={collapsed} />

        {/* ── Footer: Manage Tabs + Collapse ─────────────────────────────── */}
        <div
          style={{
            padding: '8px',
            borderTop: '1px solid var(--color-border)',
            flexShrink: 0,
            display: 'flex',
            flexDirection: 'column',
            gap: 2,
          }}
        >
          {/* Manage Tabs button */}
          <button
            onClick={() => setManageTabsOpen(true)}
            title={collapsed ? 'Manage Tabs' : undefined}
            aria-label="Manage Tabs"
            style={{
              width: '100%',
              display: 'flex',
              alignItems: 'center',
              justifyContent: collapsed ? 'center' : 'flex-start',
              gap: 10,
              padding: collapsed ? '8px 0' : '8px 12px',
              borderRadius: 8,
              border: 'none',
              backgroundColor: 'transparent',
              color: hiddenTabs.size > 0 ? 'var(--color-secondary)' : 'var(--color-text-muted)',
              cursor: 'pointer',
              fontSize: 12,
              transition: 'background-color 0.15s, color 0.15s',
            }}
            onMouseEnter={(e) => {
              e.currentTarget.style.backgroundColor = 'var(--color-surface-el)';
              e.currentTarget.style.color = 'var(--color-text-secondary)';
            }}
            onMouseLeave={(e) => {
              e.currentTarget.style.backgroundColor = 'transparent';
              e.currentTarget.style.color = hiddenTabs.size > 0 ? 'var(--color-secondary)' : 'var(--color-text-muted)';
            }}
          >
            <SlidersHorizontal size={16} />
            {!collapsed && (
              <span style={{ flex: 1 }}>
                Manage Tabs
                {hiddenTabs.size > 0 && (
                  <span
                    style={{
                      marginLeft: 6,
                      fontSize: 10,
                      fontWeight: 700,
                      color: 'var(--color-secondary)',
                    }}
                  >
                    ({hiddenTabs.size})
                  </span>
                )}
              </span>
            )}
          </button>

          {/* Collapse toggle */}
          <button
            onClick={() => setCollapsed((c) => !c)}
            title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
            aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
            style={{
              width: '100%',
              display: 'flex',
              alignItems: 'center',
              justifyContent: collapsed ? 'center' : 'flex-start',
              gap: 10,
              padding: collapsed ? '8px 0' : '8px 12px',
              borderRadius: 8,
              border: 'none',
              backgroundColor: 'transparent',
              color: 'var(--color-text-muted)',
              cursor: 'pointer',
              fontSize: 12,
              transition: 'background-color 0.15s, color 0.15s',
            }}
            onMouseEnter={(e) => {
              e.currentTarget.style.backgroundColor = 'var(--color-surface-el)';
              e.currentTarget.style.color = 'var(--color-text-secondary)';
            }}
            onMouseLeave={(e) => {
              e.currentTarget.style.backgroundColor = 'transparent';
              e.currentTarget.style.color = 'var(--color-text-muted)';
            }}
          >
            {collapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
            {!collapsed && <span>Collapse</span>}
          </button>
        </div>

        {/* ── Manage Tabs Modal ────────────────────────────────────────────── */}
        <ManageTabsModal
          open={manageTabsOpen}
          onClose={() => setManageTabsOpen(false)}
          allTabs={allAvailableTabs}
          hiddenTabs={hiddenTabs}
          onToggleTab={toggleHideTab}
          onResetAll={resetHiddenTabs}
        />
      </aside>
    </>
  );
}

/* ─── SidebarButton ──────────────────────────────────────────────────────── */
function SidebarButton({
  id,
  label,
  Icon,
  isActive,
  collapsed,
  onClick,
  kbd,
  onToggleHide,
  isHiddenByUser,
}: {
  id: string;
  label: string;
  Icon: React.ElementType;
  isActive: boolean;
  collapsed: boolean;
  onClick: () => void;
  kbd?: string;
  onToggleHide?: (tabId: TabId) => void;
  isHiddenByUser?: boolean;
}) {
  const [hovered, setHovered] = useState(false);

  const handleEyeClick = useCallback(
    (e: React.MouseEvent) => {
      e.stopPropagation();
      if (onToggleHide) onToggleHide(id as TabId);
    },
    [onToggleHide, id],
  );

  return (
    <button
      onClick={onClick}
      aria-current={isActive ? 'page' : undefined}
      title={collapsed ? (kbd ? `${label} (${kbd})` : label) : undefined}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      style={{
        display: 'flex',
        alignItems: 'center',
        gap: 10,
        padding: collapsed ? '12px 0' : '10px 12px',  /* was 8px — taller target */
        minHeight: 44,                                  /* WCAG 2.5.5 touch target */
        justifyContent: collapsed ? 'center' : 'flex-start',
        borderRadius: 8,
        border: 'none',
        cursor: 'pointer',
        position: 'relative',
        backgroundColor: isActive
          ? 'rgba(16, 185, 129, 0.16)'
          : hovered
          ? 'var(--color-surface-el)'
          : 'transparent',
        boxShadow: isActive
          ? 'inset 0 0 14px rgba(16, 185, 129, 0.08)'
          : 'none',
        color: isActive
          ? 'var(--color-text)'
          : hovered
          ? 'var(--color-text)'
          : 'var(--color-text-secondary)',
        fontSize: 15,                   /* was 13 — more readable nav labels */
        fontWeight: isActive ? 600 : 500,
        transition: 'background-color 0.15s, color 0.15s, box-shadow 0.15s',
        width: '100%',
        overflow: 'hidden',
        whiteSpace: 'nowrap',
      }}
    >
      {/* Active indicator bar — thicker and more prominent */}
      {isActive && (
        <div
          style={{
            position: 'absolute',
            left: 0,
            top: '15%',
            bottom: '15%',
            width: 4,                   /* was 3 — thicker = more visible */
            borderRadius: '0 4px 4px 0',
            background: 'linear-gradient(180deg, #34d399, #10b981)',
            boxShadow: '0 0 8px rgba(52, 211, 153, 0.6)',
          }}
          aria-hidden="true"
        />
      )}
      <Icon
        size={20}                       /* was 18 — easier to see */
        aria-hidden="true"
        style={{
          color: isActive ? '#34d399' : 'currentColor',
          flexShrink: 0,
          transition: 'color 0.15s, transform 0.15s',
          transform: hovered && !isActive ? 'scale(1.08)' : 'scale(1)',
        }}
      />
      {!collapsed && <span style={{ flex: 1 }}>{label}</span>}

      {/* Eye toggle — shown on hover, not in collapsed mode */}
      {!collapsed && hovered && onToggleHide && (
        <span
          role="button"
          tabIndex={-1}
          aria-label={isHiddenByUser ? `Show ${label}` : `Hide ${label}`}
          onClick={handleEyeClick}
          style={{
            display: 'inline-flex',
            alignItems: 'center',
            justifyContent: 'center',
            padding: 2,
            borderRadius: 4,
            flexShrink: 0,
            color: isHiddenByUser ? 'var(--color-warning)' : 'var(--color-text-muted)',
            opacity: 0.7,
            transition: 'opacity 0.15s, color 0.15s',
          }}
          onMouseEnter={(e) => { e.currentTarget.style.opacity = '1'; }}
          onMouseLeave={(e) => { e.currentTarget.style.opacity = '0.7'; }}
        >
          {isHiddenByUser ? <EyeOff size={14} /> : <Eye size={14} />}
        </span>
      )}

      {!collapsed && kbd && !hovered && (
        <span className="kbd" aria-label={`Shortcut: ${kbd}`}>{kbd}</span>
      )}
      {collapsed && <span className="sr-only">{label}</span>}
    </button>
  );
}