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 (