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() {
|
||||
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 });
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
+11
@@ -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}
|
||||
|
||||
+37
-52
@@ -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({
|
||||
<div className="popup">
|
||||
<div className="popup-inner" ref={rootRef}>
|
||||
<div className="popup-header drag-region">
|
||||
<div className="popup-brand" ref={brandRef}>
|
||||
<div className="popup-brand">
|
||||
<img src={iconUrl} alt="" className="popup-brand-icon" />
|
||||
<h1>Trailhead Path Bookmarker</h1>
|
||||
<h1 title="Trailhead Path Bookmarker">Trailhead Path Bookmarker</h1>
|
||||
</div>
|
||||
<div className="header-actions" ref={actionsRef}>
|
||||
{!creatingColumn ? (
|
||||
<div className="header-actions">
|
||||
{!minimized && !creatingColumn ? (
|
||||
<button
|
||||
className="icon-button"
|
||||
onClick={() => setCreatingColumn(true)}
|
||||
title={canAddColumn ? 'Add a new column to this tab' : 'Add a tab first'}
|
||||
disabled={!canAddColumn}
|
||||
>+</button>
|
||||
) : (
|
||||
) : !minimized && creatingColumn ? (
|
||||
<div className="header-add-column">
|
||||
<input
|
||||
autoFocus
|
||||
@@ -219,7 +197,12 @@ export default function Popup({
|
||||
title="Cancel"
|
||||
>×</button>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
<button
|
||||
className="icon-button"
|
||||
onClick={onToggleMinimized}
|
||||
title={minimized ? 'Restore' : 'Minimize'}
|
||||
>{minimized ? '▢' : '−'}</button>
|
||||
<div className="popup-settings" ref={settingsRef}>
|
||||
<button
|
||||
className={`icon-button ${settingsOpen ? 'active' : ''}`}
|
||||
@@ -275,20 +258,22 @@ export default function Popup({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabBar
|
||||
tabs={tabs}
|
||||
activeTabId={activeTabId}
|
||||
columns={columns}
|
||||
stripRef={tabStripRef}
|
||||
onSelect={onSelectTab}
|
||||
onAdd={onAddTab}
|
||||
onRename={onRenameTab}
|
||||
onRemove={onRemoveTab}
|
||||
onReorder={onReorderTabs}
|
||||
onRequestConfirm={requestConfirm}
|
||||
/>
|
||||
{!minimized && (
|
||||
<TabBar
|
||||
tabs={tabs}
|
||||
activeTabId={activeTabId}
|
||||
columns={columns}
|
||||
stripRef={tabStripRef}
|
||||
onSelect={onSelectTab}
|
||||
onAdd={onAddTab}
|
||||
onRename={onRenameTab}
|
||||
onRemove={onRemoveTab}
|
||||
onReorder={onReorderTabs}
|
||||
onRequestConfirm={requestConfirm}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasNoTabs ? (
|
||||
{minimized ? null : hasNoTabs ? (
|
||||
<div className="empty-state">
|
||||
No tabs yet — click <strong>+</strong> in the tab bar to create your first.
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user