Min width = 1 column, add header minimize, Claude → PowerShell
- Width: drop the header-fits-too floor; popup width is now max(COLUMN_WIDTH, columns-required, tabs-required). The brand title in the header is allowed to ellipsize at the 216px minimum (full name preserved in tray hover and h1 title attr). - Minimize: new -/▢ button in the header next to pin. Toggling hides the tab strip + body, locks width at COLUMN_WIDTH, and lets the existing ResizeObserver collapse height to just the header. State persists in config.minimized so reopening the popup remembers it. Add-column + button is hidden while minimized. - Claude launcher: now spawns powershell.exe -NoExit -Command claude in the wt new-tab (was cmd.exe /K claude). Cmd-fallback updated to also start powershell directly when the trailing args target it.
This commit is contained in:
Binary file not shown.
@@ -296,6 +296,7 @@ function truncatePath(p) {
|
|||||||
|
|
||||||
function broadcastConfig() {
|
function broadcastConfig() {
|
||||||
if (popupWindow && !popupWindow.isDestroyed()) {
|
if (popupWindow && !popupWindow.isDestroyed()) {
|
||||||
|
const cfg = readConfig();
|
||||||
popupWindow.webContents.send('config-updated', {
|
popupWindow.webContents.send('config-updated', {
|
||||||
tabs: getTabs(),
|
tabs: getTabs(),
|
||||||
columns: getColumns(),
|
columns: getColumns(),
|
||||||
@@ -303,6 +304,7 @@ function broadcastConfig() {
|
|||||||
activeTabId: getActiveTabId(),
|
activeTabId: getActiveTabId(),
|
||||||
buttonVisibility: getButtonVisibility(),
|
buttonVisibility: getButtonVisibility(),
|
||||||
autoStart: getAutoStart(),
|
autoStart: getAutoStart(),
|
||||||
|
minimized: !!cfg.minimized,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -488,6 +490,7 @@ ipcMain.handle('get-config', () => {
|
|||||||
activeTabId: getActiveTabId(),
|
activeTabId: getActiveTabId(),
|
||||||
buttonVisibility: getButtonVisibility(),
|
buttonVisibility: getButtonVisibility(),
|
||||||
autoStart: getAutoStart(),
|
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('copy-path', (_event, text) => launchers.copyPath(text));
|
||||||
ipcMain.handle('is-claude-available', () => launchers.isClaudeAvailable());
|
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) => {
|
ipcMain.handle('set-pinned', (_event, value) => {
|
||||||
pinned = !!value;
|
pinned = !!value;
|
||||||
writeConfig({ pinned });
|
writeConfig({ pinned });
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ contextBridge.exposeInMainWorld('bookmarks', {
|
|||||||
|
|
||||||
// Window
|
// Window
|
||||||
setPinned: (value) => ipcRenderer.invoke('set-pinned', value),
|
setPinned: (value) => ipcRenderer.invoke('set-pinned', value),
|
||||||
|
setMinimized: (value) => ipcRenderer.invoke('set-minimized', value),
|
||||||
setPopupHeight: (height) => ipcRenderer.invoke('set-popup-height', height),
|
setPopupHeight: (height) => ipcRenderer.invoke('set-popup-height', height),
|
||||||
setPopupWidth: (width) => ipcRenderer.invoke('set-popup-width', width),
|
setPopupWidth: (width) => ipcRenderer.invoke('set-popup-width', width),
|
||||||
|
|
||||||
|
|||||||
+11
@@ -7,6 +7,7 @@ export default function App() {
|
|||||||
const [columns, setColumns] = useState([]);
|
const [columns, setColumns] = useState([]);
|
||||||
const [bookmarks, setBookmarks] = useState([]);
|
const [bookmarks, setBookmarks] = useState([]);
|
||||||
const [pinned, setPinned] = useState(false);
|
const [pinned, setPinned] = useState(false);
|
||||||
|
const [minimized, setMinimized] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [claudeAvailable, setClaudeAvailable] = useState(true);
|
const [claudeAvailable, setClaudeAvailable] = useState(true);
|
||||||
const [buttonVisibility, setButtonVisibility] = useState({ claude: true, terminal: true, redeploy: 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 : []);
|
setColumns(Array.isArray(cfg.columns) ? cfg.columns : []);
|
||||||
setBookmarks(Array.isArray(cfg.bookmarks) ? cfg.bookmarks : []);
|
setBookmarks(Array.isArray(cfg.bookmarks) ? cfg.bookmarks : []);
|
||||||
setPinned(!!cfg.pinned);
|
setPinned(!!cfg.pinned);
|
||||||
|
setMinimized(!!cfg.minimized);
|
||||||
setClaudeAvailable(!!hasClaude);
|
setClaudeAvailable(!!hasClaude);
|
||||||
if (cfg.buttonVisibility) {
|
if (cfg.buttonVisibility) {
|
||||||
setButtonVisibility({
|
setButtonVisibility({
|
||||||
@@ -56,6 +58,7 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (typeof payload.autoStart === 'boolean') setAutoStart(payload.autoStart);
|
if (typeof payload.autoStart === 'boolean') setAutoStart(payload.autoStart);
|
||||||
|
if (typeof payload.minimized === 'boolean') setMinimized(payload.minimized);
|
||||||
});
|
});
|
||||||
const offShown = window.bookmarks.onPopupShown(() => {
|
const offShown = window.bookmarks.onPopupShown(() => {
|
||||||
setInspectRevision((r) => r + 1);
|
setInspectRevision((r) => r + 1);
|
||||||
@@ -159,6 +162,12 @@ export default function App() {
|
|||||||
setPinned(!!next);
|
setPinned(!!next);
|
||||||
}, [pinned]);
|
}, [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) => {
|
const handleSetButtonVisibility = useCallback(async (patch) => {
|
||||||
// Optimistic update so the checkbox flips immediately even before main responds.
|
// Optimistic update so the checkbox flips immediately even before main responds.
|
||||||
setButtonVisibility((prev) => ({ ...prev, ...patch }));
|
setButtonVisibility((prev) => ({ ...prev, ...patch }));
|
||||||
@@ -211,6 +220,8 @@ export default function App() {
|
|||||||
onSetButtonVisibility={handleSetButtonVisibility}
|
onSetButtonVisibility={handleSetButtonVisibility}
|
||||||
autoStart={autoStart}
|
autoStart={autoStart}
|
||||||
onSetAutoStart={handleSetAutoStart}
|
onSetAutoStart={handleSetAutoStart}
|
||||||
|
minimized={minimized}
|
||||||
|
onToggleMinimized={handleToggleMinimized}
|
||||||
onTogglePin={handleTogglePin}
|
onTogglePin={handleTogglePin}
|
||||||
onAdd={handleAdd}
|
onAdd={handleAdd}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
|
|||||||
+25
-40
@@ -22,6 +22,8 @@ export default function Popup({
|
|||||||
onSetButtonVisibility,
|
onSetButtonVisibility,
|
||||||
autoStart,
|
autoStart,
|
||||||
onSetAutoStart,
|
onSetAutoStart,
|
||||||
|
minimized,
|
||||||
|
onToggleMinimized,
|
||||||
onTogglePin,
|
onTogglePin,
|
||||||
onAdd,
|
onAdd,
|
||||||
onEdit,
|
onEdit,
|
||||||
@@ -42,14 +44,11 @@ export default function Popup({
|
|||||||
const heightDebounceRef = useRef(null);
|
const heightDebounceRef = useRef(null);
|
||||||
const settingsRef = useRef(null);
|
const settingsRef = useRef(null);
|
||||||
const tabStripRef = useRef(null);
|
const tabStripRef = useRef(null);
|
||||||
const brandRef = useRef(null);
|
|
||||||
const actionsRef = useRef(null);
|
|
||||||
const [creatingColumn, setCreatingColumn] = useState(false);
|
const [creatingColumn, setCreatingColumn] = useState(false);
|
||||||
const [draftColumnName, setDraftColumnName] = useState('');
|
const [draftColumnName, setDraftColumnName] = useState('');
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
const [confirmRequest, setConfirmRequest] = useState(null);
|
const [confirmRequest, setConfirmRequest] = useState(null);
|
||||||
const [tabStripScrollWidth, setTabStripScrollWidth] = useState(0);
|
const [tabStripScrollWidth, setTabStripScrollWidth] = useState(0);
|
||||||
const [headerMinWidth, setHeaderMinWidth] = useState(0);
|
|
||||||
|
|
||||||
const requestConfirm = useCallback((opts) => {
|
const requestConfirm = useCallback((opts) => {
|
||||||
setConfirmRequest(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.
|
// Watch the tab strip's scrollWidth so renames or new tabs widen the popup.
|
||||||
// ResizeObserver picks up both layout changes (window width changes) and
|
// ResizeObserver picks up both layout changes (window width changes) and
|
||||||
// content changes (a tab rename growing the strip).
|
// 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)
|
// 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.
|
// or scroll horizontally inside the popup once they would push it off-screen.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (minimized) {
|
||||||
|
window.bookmarks.setPopupWidth(COLUMN_WIDTH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const colsWidth = Math.max(1, visibleColumns.length || 1) * COLUMN_WIDTH;
|
const colsWidth = Math.max(1, visibleColumns.length || 1) * COLUMN_WIDTH;
|
||||||
const tabsWidth = tabStripScrollWidth > 0 ? tabStripScrollWidth + TAB_STRIP_PADDING : 0;
|
const tabsWidth = tabStripScrollWidth > 0 ? tabStripScrollWidth + TAB_STRIP_PADDING : 0;
|
||||||
window.bookmarks.setPopupWidth(Math.max(colsWidth, tabsWidth, headerMinWidth));
|
// Floor at COLUMN_WIDTH (one column) — the long brand title in the header
|
||||||
}, [visibleColumns.length, tabStripScrollWidth, headerMinWidth]);
|
// 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 submitNewColumn = async () => {
|
||||||
const name = draftColumnName.trim();
|
const name = draftColumnName.trim();
|
||||||
@@ -184,19 +162,19 @@ export default function Popup({
|
|||||||
<div className="popup">
|
<div className="popup">
|
||||||
<div className="popup-inner" ref={rootRef}>
|
<div className="popup-inner" ref={rootRef}>
|
||||||
<div className="popup-header drag-region">
|
<div className="popup-header drag-region">
|
||||||
<div className="popup-brand" ref={brandRef}>
|
<div className="popup-brand">
|
||||||
<img src={iconUrl} alt="" className="popup-brand-icon" />
|
<img src={iconUrl} alt="" className="popup-brand-icon" />
|
||||||
<h1>Trailhead Path Bookmarker</h1>
|
<h1 title="Trailhead Path Bookmarker">Trailhead Path Bookmarker</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="header-actions" ref={actionsRef}>
|
<div className="header-actions">
|
||||||
{!creatingColumn ? (
|
{!minimized && !creatingColumn ? (
|
||||||
<button
|
<button
|
||||||
className="icon-button"
|
className="icon-button"
|
||||||
onClick={() => setCreatingColumn(true)}
|
onClick={() => setCreatingColumn(true)}
|
||||||
title={canAddColumn ? 'Add a new column to this tab' : 'Add a tab first'}
|
title={canAddColumn ? 'Add a new column to this tab' : 'Add a tab first'}
|
||||||
disabled={!canAddColumn}
|
disabled={!canAddColumn}
|
||||||
>+</button>
|
>+</button>
|
||||||
) : (
|
) : !minimized && creatingColumn ? (
|
||||||
<div className="header-add-column">
|
<div className="header-add-column">
|
||||||
<input
|
<input
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -219,7 +197,12 @@ export default function Popup({
|
|||||||
title="Cancel"
|
title="Cancel"
|
||||||
>×</button>
|
>×</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
|
<button
|
||||||
|
className="icon-button"
|
||||||
|
onClick={onToggleMinimized}
|
||||||
|
title={minimized ? 'Restore' : 'Minimize'}
|
||||||
|
>{minimized ? '▢' : '−'}</button>
|
||||||
<div className="popup-settings" ref={settingsRef}>
|
<div className="popup-settings" ref={settingsRef}>
|
||||||
<button
|
<button
|
||||||
className={`icon-button ${settingsOpen ? 'active' : ''}`}
|
className={`icon-button ${settingsOpen ? 'active' : ''}`}
|
||||||
@@ -275,6 +258,7 @@ export default function Popup({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!minimized && (
|
||||||
<TabBar
|
<TabBar
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
activeTabId={activeTabId}
|
activeTabId={activeTabId}
|
||||||
@@ -287,8 +271,9 @@ export default function Popup({
|
|||||||
onReorder={onReorderTabs}
|
onReorder={onReorderTabs}
|
||||||
onRequestConfirm={requestConfirm}
|
onRequestConfirm={requestConfirm}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{hasNoTabs ? (
|
{minimized ? null : hasNoTabs ? (
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
No tabs yet — click <strong>+</strong> in the tab bar to create your first.
|
No tabs yet — click <strong>+</strong> in the tab bar to create your first.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user