diff --git a/electron/launchers.js b/electron/launchers.js index 1029c1b..d65891d 100644 Binary files a/electron/launchers.js and b/electron/launchers.js differ diff --git a/electron/main.js b/electron/main.js index 1e36f7c..a865506 100644 --- a/electron/main.js +++ b/electron/main.js @@ -296,6 +296,7 @@ function truncatePath(p) { function broadcastConfig() { if (popupWindow && !popupWindow.isDestroyed()) { + const cfg = readConfig(); popupWindow.webContents.send('config-updated', { tabs: getTabs(), columns: getColumns(), @@ -303,6 +304,7 @@ function broadcastConfig() { activeTabId: getActiveTabId(), buttonVisibility: getButtonVisibility(), autoStart: getAutoStart(), + minimized: !!cfg.minimized, }); } } @@ -488,6 +490,7 @@ ipcMain.handle('get-config', () => { activeTabId: getActiveTabId(), buttonVisibility: getButtonVisibility(), autoStart: getAutoStart(), + minimized: !!cfg.minimized, }; }); @@ -712,6 +715,13 @@ ipcMain.handle('run-redeploy', (_event, p) => launchers.runRedeploy(p)); ipcMain.handle('copy-path', (_event, text) => launchers.copyPath(text)); ipcMain.handle('is-claude-available', () => launchers.isClaudeAvailable()); +ipcMain.handle('set-minimized', (_event, value) => { + const next = !!value; + writeConfig({ minimized: next }); + broadcastConfig(); + return next; +}); + ipcMain.handle('set-pinned', (_event, value) => { pinned = !!value; writeConfig({ pinned }); diff --git a/electron/preload.js b/electron/preload.js index 77987d1..550ef21 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -40,6 +40,7 @@ contextBridge.exposeInMainWorld('bookmarks', { // Window setPinned: (value) => ipcRenderer.invoke('set-pinned', value), + setMinimized: (value) => ipcRenderer.invoke('set-minimized', value), setPopupHeight: (height) => ipcRenderer.invoke('set-popup-height', height), setPopupWidth: (width) => ipcRenderer.invoke('set-popup-width', width), diff --git a/src/App.jsx b/src/App.jsx index 1d8e46a..2b12b7a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,6 +7,7 @@ export default function App() { const [columns, setColumns] = useState([]); const [bookmarks, setBookmarks] = useState([]); const [pinned, setPinned] = useState(false); + const [minimized, setMinimized] = useState(false); const [loading, setLoading] = useState(true); const [claudeAvailable, setClaudeAvailable] = useState(true); const [buttonVisibility, setButtonVisibility] = useState({ claude: true, terminal: true, redeploy: true }); @@ -26,6 +27,7 @@ export default function App() { setColumns(Array.isArray(cfg.columns) ? cfg.columns : []); setBookmarks(Array.isArray(cfg.bookmarks) ? cfg.bookmarks : []); setPinned(!!cfg.pinned); + setMinimized(!!cfg.minimized); setClaudeAvailable(!!hasClaude); if (cfg.buttonVisibility) { setButtonVisibility({ @@ -56,6 +58,7 @@ export default function App() { }); } if (typeof payload.autoStart === 'boolean') setAutoStart(payload.autoStart); + if (typeof payload.minimized === 'boolean') setMinimized(payload.minimized); }); const offShown = window.bookmarks.onPopupShown(() => { setInspectRevision((r) => r + 1); @@ -159,6 +162,12 @@ export default function App() { setPinned(!!next); }, [pinned]); + const handleToggleMinimized = useCallback(async () => { + setMinimized((prev) => !prev); + const next = await window.bookmarks.setMinimized(!minimized); + if (typeof next === 'boolean') setMinimized(next); + }, [minimized]); + const handleSetButtonVisibility = useCallback(async (patch) => { // Optimistic update so the checkbox flips immediately even before main responds. setButtonVisibility((prev) => ({ ...prev, ...patch })); @@ -211,6 +220,8 @@ export default function App() { onSetButtonVisibility={handleSetButtonVisibility} autoStart={autoStart} onSetAutoStart={handleSetAutoStart} + minimized={minimized} + onToggleMinimized={handleToggleMinimized} onTogglePin={handleTogglePin} onAdd={handleAdd} onEdit={handleEdit} diff --git a/src/components/Popup.jsx b/src/components/Popup.jsx index dbed37b..7ce7e30 100644 --- a/src/components/Popup.jsx +++ b/src/components/Popup.jsx @@ -22,6 +22,8 @@ export default function Popup({ onSetButtonVisibility, autoStart, onSetAutoStart, + minimized, + onToggleMinimized, onTogglePin, onAdd, onEdit, @@ -42,14 +44,11 @@ export default function Popup({ const heightDebounceRef = useRef(null); const settingsRef = useRef(null); const tabStripRef = useRef(null); - const brandRef = useRef(null); - const actionsRef = 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 [headerMinWidth, setHeaderMinWidth] = useState(0); const requestConfirm = useCallback((opts) => { setConfirmRequest(opts); @@ -90,34 +89,6 @@ export default function Popup({ }; }, []); - // Measure the popup header's intrinsic width so the brand never gets - // ellipsized. The h1 has overflow:hidden + text-overflow:ellipsis to keep - // it from forcing the popup wider than its content; we ask for that width - // explicitly through scrollWidth (which reports the full text width even - // when the element is clipped). - useEffect(() => { - if (!brandRef.current || !actionsRef.current) return undefined; - const update = () => { - const brand = brandRef.current; - const actions = actionsRef.current; - if (!brand || !actions) return; - const iconEl = brand.querySelector('img'); - const titleEl = brand.querySelector('h1'); - const iconW = iconEl ? iconEl.offsetWidth : 0; - const titleW = titleEl ? titleEl.scrollWidth : 0; - const brandW = iconW + (titleW > 0 ? 8 + titleW : 0); - const actionsW = actions.scrollWidth; - // 28px = popup-inner horizontal padding (14 each side); 8px = gap - // between brand and actions inside popup-header. - setHeaderMinWidth(brandW + 8 + actionsW + 28); - }; - update(); - const ro = new ResizeObserver(update); - ro.observe(brandRef.current); - ro.observe(actionsRef.current); - return () => ro.disconnect(); - }, []); - // 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). @@ -142,10 +113,17 @@ export default function Popup({ // 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(() => { + if (minimized) { + window.bookmarks.setPopupWidth(COLUMN_WIDTH); + return; + } 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, headerMinWidth)); - }, [visibleColumns.length, tabStripScrollWidth, headerMinWidth]); + // Floor at COLUMN_WIDTH (one column) — the long brand title in the header + // is allowed to ellipsize when the popup is at this minimum; the full + // name is preserved in the tray hover. + window.bookmarks.setPopupWidth(Math.max(COLUMN_WIDTH, colsWidth, tabsWidth)); + }, [visibleColumns.length, tabStripScrollWidth, minimized]); const submitNewColumn = async () => { const name = draftColumnName.trim(); @@ -184,19 +162,19 @@ export default function Popup({
-
+
-

Trailhead Path Bookmarker

+

Trailhead Path Bookmarker

-
- {!creatingColumn ? ( +
+ {!minimized && !creatingColumn ? ( - ) : ( + ) : !minimized && creatingColumn ? (
×
- )} + ) : null} +
- + {!minimized && ( + + )} - {hasNoTabs ? ( + {minimized ? null : hasNoTabs ? (
No tabs yet — click + in the tab bar to create your first.