← back to Norma
components/ManageTabsModal.tsx
236 lines
'use client';
import { useState, useMemo } from 'react';
import { Eye, EyeOff, Search, RotateCcw } from 'lucide-react';
import type { TabId } from './Sidebar';
import ResizableModal from './shared/ResizableModal';
/* ─── Types ──────────────────────────────────────────────────────────────── */
interface TabMeta {
id: TabId;
label: string;
section: string;
}
interface ManageTabsModalProps {
open: boolean;
onClose: () => void;
allTabs: TabMeta[];
hiddenTabs: Set<TabId>;
onToggleTab: (tabId: TabId) => void;
onResetAll: () => void;
}
/* ─── Section display names ──────────────────────────────────────────────── */
const SECTION_LABELS: Record<string, string> = {
workspace: 'Workspace',
'email-sends': 'Email & Outreach',
intelligence: 'Intelligence',
geo: 'Geo Intelligence',
pulse: 'Pulse',
congress: 'Congress',
nexus: 'Nexus',
agents: 'AI Agents',
universal: 'Universal',
};
/* ─── Component ──────────────────────────────────────────────────────────── */
export default function ManageTabsModal({
open,
onClose,
allTabs,
hiddenTabs,
onToggleTab,
onResetAll,
}: ManageTabsModalProps) {
const [search, setSearch] = useState('');
// Group tabs by section
const sections = useMemo(() => {
const map = new Map<string, TabMeta[]>();
for (const tab of allTabs) {
const filtered = search.trim()
? tab.label.toLowerCase().includes(search.trim().toLowerCase())
: true;
if (!filtered) continue;
const list = map.get(tab.section) || [];
list.push(tab);
map.set(tab.section, list);
}
return map;
}, [allTabs, search]);
const hiddenCount = hiddenTabs.size;
if (!open) return null;
return (
<ResizableModal
modalId="manage-tabs"
title={
<div>
<div style={{ fontSize: 16, fontWeight: 700 }}>Manage Tabs</div>
<div style={{ fontSize: 11, color: 'var(--color-text-muted)', fontWeight: 400, marginTop: 2 }}>
{hiddenCount === 0
? 'All tabs visible'
: `${hiddenCount} tab${hiddenCount === 1 ? '' : 's'} hidden`}
</div>
</div>
}
onClose={onClose}
defaultWidth={520}
>
{/* Search + Reset */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 12,
}}
>
<div style={{ position: 'relative', flex: 1 }}>
<Search
size={14}
style={{
position: 'absolute',
left: 10,
top: '50%',
transform: 'translateY(-50%)',
color: 'var(--color-text-muted)',
pointerEvents: 'none',
}}
/>
<input
type="text"
className="input"
placeholder="Filter tabs..."
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{
paddingLeft: 32,
fontSize: 13,
height: 34,
width: '100%',
}}
/>
</div>
{hiddenCount > 0 && (
<button
type="button"
onClick={onResetAll}
className="btn btn-ghost btn-sm"
style={{
fontSize: 12,
gap: 4,
flexShrink: 0,
whiteSpace: 'nowrap',
}}
>
<RotateCcw size={13} />
Show All
</button>
)}
</div>
{/* Tab list */}
<div>
{Array.from(sections.entries()).map(([sectionId, tabs]) => (
<div key={sectionId} style={{ marginBottom: 12 }}>
<div
style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: 'var(--color-text-muted)',
padding: '6px 8px 4px',
}}
>
{SECTION_LABELS[sectionId] || sectionId}
</div>
{tabs.map((tab) => {
const isHidden = hiddenTabs.has(tab.id);
return (
<button
key={tab.id}
type="button"
onClick={() => onToggleTab(tab.id)}
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
width: '100%',
padding: '8px 10px',
borderRadius: 'var(--radius-sm)',
border: 'none',
cursor: 'pointer',
backgroundColor: 'transparent',
color: isHidden
? 'var(--color-text-muted)'
: 'var(--color-text)',
fontSize: 13,
fontWeight: 500,
textAlign: 'left',
opacity: isHidden ? 0.5 : 1,
transition: 'background-color 0.15s, opacity 0.15s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor =
'var(--color-surface-el)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
{isHidden ? (
<EyeOff
size={15}
style={{ color: 'var(--color-text-muted)', flexShrink: 0 }}
/>
) : (
<Eye
size={15}
style={{ color: 'var(--color-primary)', flexShrink: 0 }}
/>
)}
<span style={{ flex: 1 }}>{tab.label}</span>
{isHidden && (
<span
style={{
fontSize: 10,
color: 'var(--color-text-muted)',
padding: '1px 6px',
borderRadius: 10,
border: '1px solid var(--color-border)',
}}
>
hidden
</span>
)}
</button>
);
})}
</div>
))}
{sections.size === 0 && (
<div
style={{
textAlign: 'center',
padding: '24px 0',
color: 'var(--color-text-muted)',
fontSize: 13,
}}
>
No tabs match your search
</div>
)}
</div>
</ResizableModal>
);
}