import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Column from './Column.jsx'; import TabBar from './TabBar.jsx'; import ConfirmDialog from './ConfirmDialog.jsx'; const COLUMN_WIDTH = 216; // Padding budget so the popup doesn't clip the tab strip: popup-inner has // 14px each side and the tab-bar adds 2px breathing room on either side. const TAB_STRIP_PADDING = 32; export default function Popup({ tabs, activeTabId, columns, bookmarks, pinned, claudeAvailable, inspectRevision, recentColors, buttonVisibility, onSetButtonVisibility, autoStart, onSetAutoStart, onTogglePin, onAdd, onEdit, onRemove, onMoveBookmark, onAddColumn, onRenameColumn, onRemoveColumn, onReorderColumns, onMoveColumnToTab, onAddTab, onRenameTab, onRemoveTab, onReorderTabs, onSelectTab, }) { const rootRef = useRef(null); const heightDebounceRef = useRef(null); const settingsRef = useRef(null); const tabStripRef = useRef(null); const [creatingColumn, setCreatingColumn] = useState(false); const [draftColumnName, setDraftColumnName] = useState(''); const [settingsOpen, setSettingsOpen] = useState(false); const [confirmRequest, setConfirmRequest] = useState(null); const [tabStripScrollWidth, setTabStripScrollWidth] = useState(0); const requestConfirm = useCallback((opts) => { setConfirmRequest(opts); }, []); useEffect(() => { if (!settingsOpen) return undefined; const onClick = (e) => { if (settingsRef.current && !settingsRef.current.contains(e.target)) setSettingsOpen(false); }; document.addEventListener('mousedown', onClick); return () => document.removeEventListener('mousedown', onClick); }, [settingsOpen]); // Columns belonging to the active tab. Keep stable order from the global // columns array so cross-tab moves don't shuffle siblings. const visibleColumns = useMemo( () => columns.filter((c) => c.tabId === activeTabId), [columns, activeTabId], ); // Auto-fit window height to inner content (capped in main). useEffect(() => { if (!rootRef.current) return; const observer = new ResizeObserver((entries) => { for (const entry of entries) { const h = entry.target.offsetHeight; if (heightDebounceRef.current) clearTimeout(heightDebounceRef.current); heightDebounceRef.current = setTimeout(() => { window.bookmarks.setPopupHeight(h); }, 100); } }); observer.observe(rootRef.current); return () => { observer.disconnect(); if (heightDebounceRef.current) clearTimeout(heightDebounceRef.current); }; }, []); // Watch the tab strip's scrollWidth so renames or new tabs widen the popup. // ResizeObserver picks up both layout changes (window width changes) and // content changes (a tab rename growing the strip). useEffect(() => { if (!tabStripRef.current) return undefined; const update = () => { const el = tabStripRef.current; if (!el) return; setTabStripScrollWidth(el.scrollWidth); }; update(); const observer = new ResizeObserver(update); observer.observe(tabStripRef.current); // Re-measure after children mutate (tabs added / renamed) so growth shows // up even when the strip element itself hasn't resized yet. const mutation = new MutationObserver(update); mutation.observe(tabStripRef.current, { childList: true, subtree: true, characterData: true }); return () => { observer.disconnect(); mutation.disconnect(); }; }, []); // Resize window width to max(columns-required, tabs-required). Main clamps // to the screen's work area, so wide tab strips either fit (most monitors) // or scroll horizontally inside the popup once they would push it off-screen. useEffect(() => { const colsWidth = Math.max(1, visibleColumns.length || 1) * COLUMN_WIDTH; const tabsWidth = tabStripScrollWidth > 0 ? tabStripScrollWidth + TAB_STRIP_PADDING : 0; window.bookmarks.setPopupWidth(Math.max(colsWidth, tabsWidth)); }, [visibleColumns.length, tabStripScrollWidth]); const submitNewColumn = async () => { const name = draftColumnName.trim(); if (!name) { setCreatingColumn(false); setDraftColumnName(''); return; } await onAddColumn(name); setCreatingColumn(false); setDraftColumnName(''); }; const moveColumn = (id, delta) => { // Reorder within the active tab only — convert local delta into a global // ids array that preserves all other tabs' columns in place. const visibleIds = visibleColumns.map((c) => c.id); const idx = visibleIds.indexOf(id); const target = idx + delta; if (idx < 0 || target < 0 || target >= visibleIds.length) return; const swappedVisible = visibleIds.slice(); [swappedVisible[idx], swappedVisible[target]] = [swappedVisible[target], swappedVisible[idx]]; const visibleSet = new Set(visibleIds); let visiblePtr = 0; const next = columns.map((c) => { if (visibleSet.has(c.id)) { const replacementId = swappedVisible[visiblePtr++]; return replacementId; } return c.id; }); onReorderColumns(next); }; const hasNoTabs = tabs.length === 0; const hasNoColumns = !hasNoTabs && visibleColumns.length === 0; const canAddColumn = !hasNoTabs && !!activeTabId; return (

Folder Bookmarks

{!creatingColumn ? ( ) : (
setDraftColumnName(e.target.value)} placeholder="Column name" onKeyDown={(e) => { if (e.key === 'Enter') submitNewColumn(); else if (e.key === 'Escape') { setCreatingColumn(false); setDraftColumnName(''); } }} />
)}
{settingsOpen && (
Row buttons
Startup
)}
{hasNoTabs ? (
No tabs yet — click + in the tab bar to create your first.
) : hasNoColumns ? (
No columns in this tab — click + in the header to add one.
) : (
{visibleColumns.map((col, idx) => ( b.columnId === col.id)} claudeAvailable={claudeAvailable} inspectRevision={inspectRevision} recentColors={recentColors} buttonVisibility={buttonVisibility} canMoveLeft={idx > 0} canMoveRight={idx < visibleColumns.length - 1} onAdd={onAdd} onEdit={onEdit} onRemove={onRemove} onMoveBookmark={onMoveBookmark} onRenameColumn={onRenameColumn} onRemoveColumn={onRemoveColumn} onMoveColumnToTab={onMoveColumnToTab} onMoveLeft={() => moveColumn(col.id, -1)} onMoveRight={() => moveColumn(col.id, 1)} onRequestConfirm={requestConfirm} /> ))}
)}
{confirmRequest && ( setConfirmRequest(null)} onConfirm={() => { const fn = confirmRequest.onConfirm; setConfirmRequest(null); if (typeof fn === 'function') fn(); }} /> )}
); }