← back to Norma

components/AppShell.tsx

816 lines

'use client';

import { useState, useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import { LogOut, Menu, LayoutGrid, LayoutDashboard, Newspaper, Megaphone, Award, MoreHorizontal } from 'lucide-react';
import GlobalSearch from './GlobalSearch';
import { AuthProvider, useAuth } from './AuthProvider';
import { ToastProvider } from './ToastProvider';
import { OrgProvider, useOrg } from './OrgProvider';
import Sidebar, { type TabId } from './Sidebar';
import NikkiShell from './NikkiShell';
import DashboardTab from './dashboard/DashboardTab';
import DraftsTab from './drafts/DraftsTab';
import PetitionsTab from './petitions/PetitionsTab';
import GrantsTab from './grants/GrantsTab';
import FundingExplorer from './grants/FundingExplorer';
import IntegrationsTab from './IntegrationsTab';
import ApisTab from './apis/ApisTab';
import NewsTab from './NewsTab';
import CypressAwardsTab from './CypressAwardsTab';
import CollaborationsTab from './collaborations/CollaborationsTab';
import PartnersTab from './partners/PartnersTab';
import OldLeadsTab from './leads/OldLeadsTab';
import HeyPartnerTab from './outreach/HeyPartnerTab';
import OutreachPlaybookTab from './outreach/OutreachPlaybookTab';
import CommunityTab from './community/CommunityTab';
import PetitionIdeas from './scraper/PetitionIdeas';
import XSessionsTab from './xsessions/XSessionsTab';
import LibraryTab from './library/LibraryTab';
import SettingsTab from './settings/SettingsTab';
import ZipReportCard from './geo/ZipReportCard';
import HotspotMap from './geo/HotspotMap';
import AdvocacyTargets from './geo/AdvocacyTargets';
import CrossRefExplorer from './geo/CrossRefExplorer';
import TrendPulse from './geo/TrendPulse';
import MoveOnImport from './geo/MoveOnImport';
import PulseFeed from './pulse/PulseFeed';
import IdeaMindMap from './graph/IdeaMindMap';
import PartnerTree from './graph/PartnerTree';
import ChatBar from './ChatBar';
import CongressTab from './congress/CongressTab';
import OpportunityMap from './congress/OpportunityMap';
import AgentAppsHub from './agents/AgentAppsHub';
import OrgOnboarding from './agents/OrgOnboarding';
import PetitionBuilder from './agents/PetitionBuilder';
import GrantFinder from './agents/GrantFinder';
import AdvocacyToolkit from './agents/AdvocacyToolkit';
import MoveOnLauncher from './petitions/MoveOnLauncher';
import PetitionCreator from './petitions/PetitionCreator';
import TrendingPetitions from './petitions/TrendingPetitions';
import PipelineTab from './pipeline/PipelineTab';
import TopicRadar from './radar/TopicRadar';
import StateChoropleth from './geo/StateChoropleth';
import TopicStateGrid from './geo/TopicStateGrid';
import StatementsTab from './statements/StatementsTab';
import PulseGlobe from './globe/PulseGlobe';
import EnhancedMindMap from './graph/EnhancedMindMap';
import DataSourcesTab from './datasource/DataSourcesTab';
import OnBoardTab from './onboard/OnBoardTab';
import JournalistHub from './journalists/JournalistHub';
import ContactsTab from './contacts/ContactsTab';
import NexusExplorer from './nexus/NexusExplorer';
import PowerMap from './nexus/PowerMap';
import OpportunityFinder from './nexus/OpportunityFinder';
import DeepInsights from './nexus/DeepInsights';
import PulseHeatMap from './pulse/PulseHeatMap';
import LiveNews from './news/LiveNews';
import VisualizationHub from './viz/VisualizationHub';
import ConnectionTree from './viz/ConnectionTree';
import CorrelationMatrix from './viz/CorrelationMatrix';
import NetworkGraph3D from './viz/NetworkGraph3D';
import SunburstChart from './viz/SunburstChart';
import OrganizationsTab from './orgs/OrganizationsTab';
import FoundationsTab from './foundations/FoundationsTab';
import DonationsTab from './donations/DonationsTab';
import OrgConnections from './connections/OrgConnections';
import PowerIntelligenceTab from './intelligence/PowerIntelligenceTab';
import InTheNewsTab from './intelligence/InTheNewsTab';
import EmailSendsTab from './email-sends/EmailSendsTab';
import EmailAnalyzer from './email-analyzer/EmailAnalyzer';
import SavedAnalyses from './email-analyzer/SavedAnalyses';
import GmailCRMTab from './gmail/GmailCRMTab';
// AgeThemeSlider + InboxThemeBanner imports removed — now live in app/layout.tsx
// via <TopThemeBar />. AppShell only carries authenticated-only widgets.
import ApiRegistryButton from './api-registry/ApiRegistryButton';
import ImpersonateMenu from './ImpersonateMenu';
import PriceTrackerTab from './price-tracker/PriceTrackerTab';
import SocialTab from './social/SocialTab';

/* ─── Inner shell (needs auth context) ──────────────────────────────────── */
const APP_LINKS = [
  { name: 'Hub', port: 7480, color: '#0891b2', icon: 'H' },
  { name: 'Norma', port: 7400, color: '#10b981', icon: 'N' },
  { name: 'Grant', port: 7450, color: '#059669', icon: 'G' },
  { name: 'Patty', port: 7460, color: '#7c3aed', icon: 'P' },
  { name: 'Freddy', port: 7470, color: '#d97706', icon: 'F' },
];
const CURRENT_PORT = 7400;

function Shell() {
  const { user, role, logout } = useAuth();
  const { org, orgs, switchOrg } = useOrg();

  // Role-based shell routing
  if (role === 'staff') {
    return <NikkiShell />;
  }
  if (role === 'pulse') {
    // Pulse users should be on /pulse route, redirect if they end up here
    if (typeof window !== 'undefined') {
      window.location.href = '/pulse';
    }
    return null;
  }
  const [activeTab, setActiveTab] = useState<TabId>('dashboard');
  const [geoZip, setGeoZip] = useState('');
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [showAppSwitcher, setShowAppSwitcher] = useState(false);
  const [agentOrgId, setAgentOrgId] = useState('');
  const [journalistNavId, setJournalistNavId] = useState<string | null>(null);

  function handleTabChange(tab: TabId) {
    setActiveTab(tab);
    setSidebarOpen(false);
    // Clear journalist nav ID on all normal tab changes (sidebar clicks)
    setJournalistNavId(null);
  }

  /* ── Deep-link from Supervisor's GlobalSearch: ?tab=foo&id=bar ─────────── */
  const searchParams = useSearchParams();
  useEffect(() => {
    const tab = searchParams.get('tab') as TabId | null;
    const id = searchParams.get('id');
    if (!tab) return;
    // For journalist deep-links, set the journalistNavId so the JournalistsTab can focus it
    if (tab === 'journalists' && id) setJournalistNavId(id);
    setActiveTab(tab);
    // Note: we don't strip the params from the URL — the user can refresh and land back here.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  /* ── Keyboard shortcuts for sidebar navigation ──────────────────────────── */
  useEffect(() => {
    const KBD_NAV: Record<string, TabId> = {
      h: 'dashboard',
      d: 'drafts',
      p: 'petitions',
      r: 'topic-radar',
      l: 'pipeline',
      n: 'contacts',
    };

    function handleKeyDown(e: KeyboardEvent) {
      // Skip if modifier keys are held, or user is typing in an input/textarea/contenteditable
      if (e.metaKey || e.ctrlKey || e.altKey) return;
      const tag = (e.target as HTMLElement)?.tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
      if ((e.target as HTMLElement)?.isContentEditable) return;

      const tab = KBD_NAV[e.key.toLowerCase()];
      if (tab) {
        e.preventDefault();
        handleTabChange(tab);
      }
    }

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, []);

  function renderPanel() {
    /* ── No org selected — show welcome/org picker ─────────────────────── */
    if (!org) {
      return (
        <div className="flex flex-col items-center justify-center py-20 gap-6 max-w-lg mx-auto">
          <div
            style={{
              width: 56,
              height: 56,
              borderRadius: 16,
              background: 'linear-gradient(135deg, #10b981, #059669)',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              boxShadow: '0 4px 16px rgba(16, 185, 129, 0.3)',
            }}
          >
            <span style={{ color: '#fff', fontSize: 22, fontWeight: 900 }}>N</span>
          </div>
          <div className="text-center">
            <h1 className="text-xl font-bold mb-2" style={{ color: 'var(--color-text)' }}>
              Welcome to Norma
            </h1>
            <p className="text-sm" style={{ color: 'var(--color-text-muted)' }}>
              Select an organization from the sidebar to get started. Each org has its own data, settings, and workflows.
            </p>
          </div>
          {orgs.length > 0 && (
            <div className="w-full space-y-2">
              <p className="text-xs font-semibold uppercase tracking-wider text-center" style={{ color: 'var(--color-text-muted)' }}>
                Available Organizations
              </p>
              {orgs.map((o) => (
                <button
                  key={o.id}
                  type="button"
                  onClick={() => switchOrg(o.id)}
                  className="w-full card"
                  style={{
                    display: 'flex',
                    alignItems: 'center',
                    gap: 12,
                    padding: '12px 16px',
                    cursor: 'pointer',
                    border: '1px solid var(--color-border)',
                    background: 'var(--color-surface)',
                    textAlign: 'left',
                    transition: 'border-color 0.15s, background-color 0.15s',
                  }}
                  onMouseEnter={(e) => { e.currentTarget.style.borderColor = o.brand_color; e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
                  onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--color-border)'; e.currentTarget.style.backgroundColor = 'var(--color-surface)'; }}
                >
                  <div
                    style={{
                      width: 36,
                      height: 36,
                      borderRadius: 10,
                      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 ? 9 : 13, fontWeight: 800 }}>
                      {(o.short_name || o.org_name.slice(0, 2)).slice(0, 4)}
                    </span>
                  </div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--color-text)', lineHeight: 1.2 }}>
                      {o.org_name}
                    </div>
                    <div style={{ fontSize: 11, color: 'var(--color-text-muted)', lineHeight: 1.3, marginTop: 2 }}>
                      {o.address_city}{o.address_city && o.address_state ? ', ' : ''}{o.address_state}
                      {o.focus_areas?.length > 0 ? ` — ${o.focus_areas.slice(0, 2).join(', ')}` : ''}
                    </div>
                  </div>
                </button>
              ))}
            </div>
          )}
        </div>
      );
    }

    switch (activeTab) {
      case 'dashboard':  return <DashboardTab onNavigate={(tab) => handleTabChange(tab as TabId)} />;
      case 'drafts':     return <DraftsTab onNavigateToSettings={() => handleTabChange('settings')} />;
      case 'news':       return <NewsTab onNavigate={(tab) => handleTabChange(tab as TabId)} onNavigateToJournalist={(name) => { setJournalistNavId(`name:${name}`); setActiveTab('journalists'); }} />;
      case 'petitions':       return <PetitionsTab onNavigate={(tab) => handleTabChange(tab as TabId)} />;
      case 'petition-ideas':  return <PetitionIdeas />;
      case 'grants':       return <GrantsTab onNavigate={(tab) => handleTabChange(tab as TabId)} />;
      case 'funding':      return <FundingExplorer />;
      case 'collaborations': return <CollaborationsTab />;
      case 'partners':     return <PartnersTab onNavigate={(tab) => handleTabChange(tab as TabId)} />;
      case 'oldleads':     return <OldLeadsTab />;
      case 'heypartner':   return <HeyPartnerTab />;
      case 'playbook':     return <OutreachPlaybookTab />;
      case 'community':    return <CommunityTab />;
      case 'xsessions':    return <XSessionsTab />;
      case 'library':       return <LibraryTab />;
      case 'apis':           return <ApisTab />;
      case 'integrations':  return <IntegrationsTab />;
      case 'awards':        return <CypressAwardsTab onNavigateToJournalist={(name) => { setJournalistNavId(`name:${name}`); setActiveTab('journalists'); }} />;
      case 'settings':      return <SettingsTab />;
      case 'geo-report':
        return (
          <ZipReportCard
            initialZip={geoZip}
            onNavigateToZip={(zip) => { setGeoZip(zip); setActiveTab('geo-report'); }}
          />
        );
      case 'geo-hotspots':
        return (
          <HotspotMap
            onNavigateToZip={(zip) => { setGeoZip(zip); setActiveTab('geo-report'); }}
          />
        );
      case 'geo-advocacy':  return <AdvocacyTargets />;
      case 'geo-scatter':   return <CrossRefExplorer />;
      case 'geo-trends':    return <TrendPulse />;
      case 'geo-moveon':    return <MoveOnImport />;
      case 'pulse-feed':    return <PulseFeed />;
      case 'pulse-globe':        return <PulseGlobe />;
      case 'pulse-mindmap':     return <EnhancedMindMap />;
      case 'pulse-datasources': return <DataSourcesTab />;
      case 'pulse-onboard':     return <OnBoardTab />;
      case 'moveon-launch': return <MoveOnLauncher />;
      case 'petition-creator': return <PetitionCreator />;
      case 'trending-petitions': return <TrendingPetitions />;
      case 'journalists': return <JournalistHub initialJournalistId={journalistNavId} />;
      case 'contacts': return <ContactsTab />;
      case 'topic-radar':    return <TopicRadar onNavigate={(tab) => handleTabChange(tab as TabId)} />;
      case 'statements':     return <StatementsTab />;
      case 'pipeline':       return <PipelineTab />;
      case 'geo-choropleth': return <StateChoropleth />;
      case 'geo-topic-grid': return <TopicStateGrid />;
      case 'idea-mindmap':  return <IdeaMindMap />;
      case 'partner-tree':  return <PartnerTree />;
      case 'organizations': return <OrganizationsTab />;
      case 'foundations':    return <FoundationsTab />;
      case 'donations':     return <DonationsTab />;
      case 'org-connections': return <OrgConnections />;
      case 'congress':        return <CongressTab onNavigate={(tab) => handleTabChange(tab as TabId)} />;
      case 'opportunity-map': return <OpportunityMap />;
      case 'nexus-explorer':       return <NexusExplorer />;
      case 'nexus-power-map':      return <PowerMap />;
      case 'nexus-opportunities':  return <OpportunityFinder onNavigateToJournalist={(id) => { setJournalistNavId(id); setActiveTab('journalists'); }} />;
      case 'nexus-deep-insights':  return <DeepInsights />;
      case 'viz-hub':              return <VisualizationHub />;
      case 'viz-tree':             return <ConnectionTree />;
      case 'viz-matrix':           return <CorrelationMatrix />;
      case 'viz-3d':               return <NetworkGraph3D />;
      case 'viz-sunburst':         return <SunburstChart />;
      case 'pulse-heatmap':        return <PulseHeatMap />;
      case 'live-news':            return <LiveNews />;
      case 'power-intelligence':   return <PowerIntelligenceTab />;
      case 'in-the-news':          return <InTheNewsTab />;
      case 'email-sends':           return <EmailSendsTab />;
      case 'email-statements':      return <EmailSendsTab initialType="statements" />;
      case 'email-donations':       return <EmailSendsTab initialType="donations" />;
      case 'email-breaking':        return <EmailSendsTab initialType="breaking" />;
      case 'email-education':       return <EmailSendsTab initialType="education" />;
      case 'email-workshops':       return <EmailSendsTab initialType="workshops" />;
      case 'email-congress':        return <EmailSendsTab initialType="congress" />;
      case 'email-agencies':        return <EmailSendsTab initialType="agencies" />;
      case 'email-journalists':     return <EmailSendsTab initialType="journalists" />;
      case 'email-analyzer':        return <EmailAnalyzer />;
      case 'email-analyzer-saved':
        return (
          <SavedAnalyses
            onOpen={(sid) => {
              try { sessionStorage.setItem('norma-analyzer-load-session', sid); } catch { /* noop */ }
              handleTabChange('email-analyzer');
            }}
          />
        );
      case 'price-tracker':         return <PriceTrackerTab />;
      case 'social-overview':       return <SocialTab initialPanel="overview" />;
      case 'social-compose':        return <SocialTab initialPanel="compose" />;
      case 'social-schedule':       return <SocialTab initialPanel="schedule" />;
      case 'social-analytics':      return <SocialTab initialPanel="analytics" />;
      case 'social-competitors':    return <SocialTab initialPanel="competitors" />;
      case 'social-scoreboard':     return <SocialTab initialPanel="scoreboard" />;
      case 'social-accounts':       return <SocialTab initialPanel="accounts" />;
      case 'clients':
        return (
          <div className="p-5">
            <h2 className="text-lg font-semibold" style={{ color: 'var(--color-text)' }}>Clients & Borrowers</h2>
            <p className="text-sm mt-2" style={{ color: 'var(--color-text-muted)' }}>Constituent management coming soon.</p>
          </div>
        );
      case 'gmail-crm':          return <GmailCRMTab />;
      case 'agent-hub':
        return (
          <AgentAppsHub
            onNavigate={(tab) => handleTabChange(tab as TabId)}
            selectedOrgId={agentOrgId}
            onOrgChange={setAgentOrgId}
          />
        );
      case 'agent-onboard':
        return (
          <OrgOnboarding
            onNavigate={(tab) => handleTabChange(tab as TabId)}
            onOrgChange={setAgentOrgId}
          />
        );
      case 'agent-petition':
        return (
          <PetitionBuilder
            selectedOrgId={agentOrgId}
            onOrgChange={setAgentOrgId}
          />
        );
      case 'agent-grants':
        return (
          <GrantFinder
            selectedOrgId={agentOrgId}
            onOrgChange={setAgentOrgId}
          />
        );
      case 'agent-advocacy':
        return (
          <AdvocacyToolkit
            selectedOrgId={agentOrgId}
            onOrgChange={setAgentOrgId}
          />
        );
      default:
        return (
          <div className="flex flex-col items-center justify-center py-20 gap-3" style={{ color: 'var(--color-text-muted)' }}>
            <p className="text-lg font-semibold">Tab not found</p>
            <p className="text-sm">The selected view is not available.</p>
          </div>
        );
    }
  }

  /* Tab label for the top bar */
  const TAB_LABELS: Record<TabId, string> = {
    dashboard: 'Dashboard',
    drafts: 'Drafts',
    news: 'News Feed',
    petitions: 'Petitions',
    'petition-ideas': 'Petition Ideas',
    grants: 'Grants',
    funding: 'AI Funding Explorer',
    collaborations: 'Collaborations',
    partners: 'Current Partners',
    oldleads: 'Old Leads',
    heypartner: 'Hey Partner',
    playbook: 'Outreach Playbook',
    community: 'Community Pulse',
    xsessions: 'X Sessions',
    library: 'Library',
    apis: 'API Connections',
    integrations: 'Integrations',
    awards: 'Awards',
    settings: 'Settings',
    'geo-report':   'Zip Report Card',
    'geo-hotspots': 'Hotspot Explorer',
    'geo-advocacy': 'Advocacy Targets',
    'geo-scatter':  'Cross-Reference Explorer',
    'geo-trends':   'Trend Pulse',
    'geo-moveon':   'MoveOn Petition Import',
    'pulse-feed':   'Pulse of America',
    'idea-mindmap': 'Idea Mind Map',
    'partner-tree': 'Partner Discovery Tree',
    organizations:    'Organizations',
    foundations:      'Foundations',
    donations:        'Donations',
    congress:         'Congress Members',
    'opportunity-map': 'Opportunity Map',
    'agent-hub':       'Agent Apps Hub',
    'agent-onboard':   'Organization Onboarding',
    'agent-petition':  'AI Petition Builder',
    'agent-grants':    'AI Grant Finder',
    'agent-advocacy':  'Advocacy Toolkit',
    'moveon-launch':   'MoveOn Petition Launcher',
    'topic-radar':     'Topic Radar — AI-Suggested Petition Topics',
    'pipeline':        'Petition Pipeline',
    'geo-choropleth':  'State Heat Map',
    'statements':      'Official Statements',
    'geo-topic-grid':  'Topic Grid',
    'pulse-globe':        'Pulse Globe — 3D World View',
    'pulse-mindmap':      'Enhanced Mind Map — Cross-Modal Connections',
    'pulse-datasources':  'Data Sources — APIs, Headlines & Statements',
    'pulse-onboard':      'OnBoard — Non-Profit Discovery',
    'petition-creator':      'Create Petition',
    'trending-petitions':    'Trending Petitions',
    'journalists':           'Journalist Intelligence Hub',
    'contacts':              'Network — LinkedIn Contacts & Connections',
    'nexus-explorer':        'Nexus Explorer — Cross-Entity Connections',
    'nexus-power-map':       'Power Map — Politician Influence Rankings',
    'nexus-opportunities':   'Opportunity Finder — Smart Petition Targeting',
    'nexus-deep-insights':   'Deep Insights — Cross-Entity Intelligence',
    'viz-hub':               'Visualization Hub — Interactive Connection Explorer',
    'viz-tree':              'Connection Tree — Hierarchical Entity Relationships',
    'viz-matrix':            'Correlation Matrix — Entity Type Interconnections',
    'viz-3d':                '3D Network — Spatial Connection Graph',
    'viz-sunburst':          'Sunburst — Radial Connection Hierarchy',
    'pulse-heatmap':         'Pulse Heat Map — Topic Intensity',
    'live-news':             'Live News — Real-Time Coverage',
    'org-connections':       'Organization Connections',
    'power-intelligence':    'Power Intelligence — Weekly Contact Recommendations',
    'in-the-news':           'In the News — Org Activity Feed',
    'email-sends':           'Email & Outreach Center',
    'email-statements':      'Email — Statements',
    'email-donations':       'Email — Donation Requests',
    'email-breaking':        'Email — Breaking News',
    'email-education':       'Email — Education Only',
    'email-workshops':       'Email — Workshops',
    'email-congress':        'Email — Letters to Congress',
    'email-agencies':        'Email — Letters to Agencies',
    'email-journalists':     'Email — Letters to Journalists',
    'email-analyzer':        'Email Analyzer — AI-Powered Rewriting',
    'email-analyzer-saved':  'Email Analyzer — Saved Analyses',
    'price-tracker':         'Price Tracker — Education Cost Intelligence',
    'clients':               'Clients & Borrowers',
    'gmail-crm':             'Gmail CRM — AI-Powered Inbox',
    'social-overview':       'Social Media Hub',
    'social-compose':        'Social Media — Compose',
    'social-schedule':       'Social Media — Schedule',
    'social-analytics':      'Social Media — Analytics',
    'social-competitors':    'Social Media — Competitors',
    'social-scoreboard':     'Social Media — Scoreboard',
    'social-accounts':       'Social Media — Accounts',
  };

  return (
    <div
      className="flex"
      style={{ backgroundColor: 'var(--color-bg)', minHeight: '100vh' }}
    >
      {/* ── Skip Navigation ───────────────────────────────────────────────── */}
      <a
        href="#main-content"
        className="sr-only focus:not-sr-only"
        style={{
          position: 'absolute',
          top: 8,
          left: 8,
          zIndex: 9999,
          padding: '8px 16px',
          backgroundColor: 'var(--color-primary)',
          color: '#fff',
          borderRadius: 'var(--radius-md)',
          fontSize: '0.875rem',
          fontWeight: 600,
          textDecoration: 'none',
        }}
      >
        Skip to main content
      </a>

      {/* ── Left Sidebar ─────────────────────────────────────────────────── */}
      <Sidebar activeTab={activeTab} onTabChange={handleTabChange} isOpen={sidebarOpen} onToggle={() => setSidebarOpen(p => !p)} />

      {/* ── Right: Header + Content ──────────────────────────────────────── */}
      <div className="flex flex-col flex-1 min-w-0">
        {/* ── Impersonation banner (shows ONLY while impersonating) ──────── */}
        <ImpersonateMenu variant="banner" />

        {/* Theme banner + age slider moved to app/layout.tsx so they render
            on every page including /login (set both before authenticating). */}

        {/* ── Top Header Bar ─────────────────────────────────────────────── */}
        <header
          className="flex items-center justify-between px-5 h-14 shrink-0"
          style={{
            backgroundColor: 'var(--color-surface)',
            borderBottom: '1px solid var(--color-border)',
          }}
        >
          {/* Left: hamburger + section name */}
          <div className="flex items-center gap-2">
            <button
              className="btn btn-ghost btn-sm"
              onClick={() => setSidebarOpen(p => !p)}
              style={{ display: 'none' }}
              aria-label="Toggle menu"
              id="mobile-menu-btn"
            >
              <Menu size={18} />
            </button>
            <h1
              className="text-sm font-semibold"
              style={{ color: 'var(--color-text)' }}
            >
              {TAB_LABELS[activeTab]}
            </h1>
          </div>

          {/* Center: global search */}
          <GlobalSearch onNavigate={(tab, id) => {
            if (tab === 'journalists' && id) {
              setJournalistNavId(id);
            }
            setActiveTab(tab as TabId);
            setSidebarOpen(false);
          }} />

          {/* Right: app switcher + user + logout */}
          <div className="flex items-center gap-3">
            {/* App Switcher */}
            <div style={{ position: 'relative' }}>
              <button
                className="btn btn-ghost btn-sm"
                onClick={() => setShowAppSwitcher(p => !p)}
                title="Switch app"
                aria-label="Switch app"
              >
                <LayoutGrid size={16} />
              </button>
              {showAppSwitcher && (
                <>
                  <div
                    style={{ position: 'fixed', inset: 0, zIndex: 49 }}
                    onClick={() => setShowAppSwitcher(false)}
                  />
                  <div
                    style={{
                      position: 'absolute',
                      top: '100%',
                      right: 0,
                      marginTop: 8,
                      padding: 8,
                      borderRadius: 12,
                      backgroundColor: 'var(--color-surface)',
                      border: '1px solid var(--color-border)',
                      boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
                      zIndex: 50,
                      minWidth: 200,
                    }}
                  >
                    <div style={{ padding: '4px 8px 8px', fontSize: 10, fontWeight: 600, color: 'var(--color-text-muted)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
                      Norma Ecosystem
                    </div>
                    {APP_LINKS.map((app) => (
                      <a
                        key={app.name}
                        href={`http://45.61.58.125:${app.port}`}
                        target="_blank"
                        rel="noopener noreferrer"
                        style={{
                          display: 'flex',
                          alignItems: 'center',
                          gap: 10,
                          padding: '8px 10px',
                          borderRadius: 8,
                          textDecoration: 'none',
                          color: 'var(--color-text)',
                          fontSize: '0.8125rem',
                          fontWeight: 500,
                          transition: 'background-color 0.15s',
                          borderLeft: app.port === CURRENT_PORT ? `3px solid ${app.color}` : '3px solid transparent',
                        }}
                        onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = 'var(--color-surface-el)'; }}
                        onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; }}
                        onClick={() => setShowAppSwitcher(false)}
                      >
                        <div
                          style={{
                            width: 28,
                            height: 28,
                            borderRadius: 8,
                            background: `linear-gradient(135deg, ${app.color}, ${app.color}dd)`,
                            display: 'flex',
                            alignItems: 'center',
                            justifyContent: 'center',
                            color: '#fff',
                            fontSize: 12,
                            fontWeight: 800,
                            flexShrink: 0,
                          }}
                        >
                          {app.icon}
                        </div>
                        <div>
                          <div style={{ lineHeight: 1.2 }}>{app.name}</div>
                          <div style={{ fontSize: 10, color: 'var(--color-text-muted)', lineHeight: 1.2 }}>
                            Port {app.port}
                          </div>
                        </div>
                      </a>
                    ))}
                  </div>
                </>
              )}
            </div>
            {user && (
              <span
                className="text-xs hidden sm:inline"
                style={{ color: 'var(--color-text-muted)' }}
              >
                {user}
              </span>
            )}
            {/* Inbox theme switcher moved to the top banner (see <InboxThemeBanner /> above). */}
            {/* Admin: View-as another user (impersonation) */}
            {role === 'admin' && <ImpersonateMenu variant="dropdown" />}
            {/* APIs & Tokens registry — admin only */}
            {role === 'admin' && <ApiRegistryButton variant="ghost" />}
            <button
              onClick={logout}
              className="btn btn-ghost btn-sm"
              aria-label="Sign out"
              title="Sign out"
            >
              <LogOut size={15} aria-hidden="true" />
              <span className="hidden sm:inline">Sign out</span>
            </button>
          </div>
        </header>

        {/* ── Main Content ───────────────────────────────────────────────── */}
        <main id="main-content" className="flex-1 overflow-auto mobile-main-content">
          {renderPanel()}
        </main>

        {/* ── Chat Bar (always visible at bottom) ───────────────────────── */}
        <ChatBar />
      </div>

      {/* ── Mobile Bottom Navigation ─────────────────────────────────────── */}
      <nav
        className="mobile-bottom-nav"
        aria-label="Mobile bottom navigation"
        style={{
          display: 'none',
          position: 'fixed',
          bottom: 0,
          left: 0,
          right: 0,
          height: 56,
          zIndex: 50,
          backgroundColor: 'var(--color-surface)',
          borderTop: '1px solid var(--color-border)',
          boxShadow: '0 -4px 16px rgba(0,0,0,0.08)',
        }}
      >
        <div
          style={{
            display: 'flex',
            alignItems: 'center',
            height: '100%',
            paddingBottom: 'env(safe-area-inset-bottom, 0px)',
          }}
        >
          {([
            { id: 'dashboard' as TabId,  label: 'Dashboard', Icon: LayoutDashboard },
            { id: 'news'      as TabId,  label: 'News',      Icon: Newspaper       },
            { id: 'petitions' as TabId,  label: 'Petitions', Icon: Megaphone       },
            { id: 'grants'    as TabId,  label: 'Grants',    Icon: Award           },
          ] as const).map(({ id, label, Icon }) => {
            const isActive = activeTab === id;
            return (
              <button
                key={id}
                type="button"
                onClick={() => handleTabChange(id)}
                aria-current={isActive ? 'page' : undefined}
                aria-label={label}
                style={{
                  flex: 1,
                  display: 'flex',
                  flexDirection: 'column',
                  alignItems: 'center',
                  justifyContent: 'center',
                  gap: 3,
                  minHeight: 44,
                  height: '100%',
                  border: 'none',
                  background: 'none',
                  cursor: 'pointer',
                  color: isActive ? 'var(--color-primary)' : 'var(--color-text-muted)',
                  transition: 'color 0.15s',
                  position: 'relative',
                }}
              >
                {isActive && (
                  <div
                    aria-hidden="true"
                    style={{
                      position: 'absolute',
                      top: 0,
                      left: '20%',
                      right: '20%',
                      height: 2,
                      borderRadius: '0 0 2px 2px',
                      background: 'linear-gradient(90deg, #34d399, #10b981)',
                      boxShadow: '0 0 6px rgba(52,211,153,0.6)',
                    }}
                  />
                )}
                <Icon size={20} aria-hidden="true" />
                <span style={{ fontSize: 10, fontWeight: isActive ? 700 : 500, lineHeight: 1 }}>
                  {label}
                </span>
              </button>
            );
          })}
          {/* More — opens sidebar */}
          <button
            type="button"
            onClick={() => setSidebarOpen(true)}
            aria-label="Open navigation menu"
            style={{
              flex: 1,
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
              justifyContent: 'center',
              gap: 3,
              minHeight: 44,
              height: '100%',
              border: 'none',
              background: 'none',
              cursor: 'pointer',
              color: 'var(--color-text-muted)',
              transition: 'color 0.15s',
            }}
          >
            <MoreHorizontal size={20} aria-hidden="true" />
            <span style={{ fontSize: 10, fontWeight: 500, lineHeight: 1 }}>More</span>
          </button>
        </div>
      </nav>
    </div>
  );
}

/* ─── Public export — wraps Shell in AuthProvider ────────────────────────── */
export default function AppShell() {
  return (
    <AuthProvider>
      <ToastProvider>
        <OrgProvider>
          <Shell />
        </OrgProvider>
      </ToastProvider>
    </AuthProvider>
  );
}