Theseus: sidebar panels open over the page or push it aside
An open sidebar panel (a web app or an extension panel) now has a bar on top with its name, Maximize/Restore (it covers the page, up to the right sidebar) and Close. How panels sit next to the page is a choice, in the new gear above + on the strip and in Settings > Sidebar: over the page, where it floats on the left of a page that keeps its width and a click in the page closes it; or pushing the page aside, where it stays open until closed. That choice replaces the pin, which only existed to keep a panel open on a page click. Also: a window laid out while minimized reads a zero-size area, so every view was left at 0x0 and the window came back blank until resized. Layout now skips a minimized window and runs again on restore.
This commit is contained in:
parent
f803491844
commit
110d0157de
7 changed files with 195 additions and 69 deletions
133
main.js
133
main.js
|
|
@ -594,7 +594,11 @@ const SETTINGS_DEFAULTS = {
|
||||||
// "p:<panelId>"), extension panels the user took off the strip, and when
|
// "p:<panelId>"), extension panels the user took off the strip, and when
|
||||||
// the open panel steps aside (Opera's rules, each one switchable).
|
// the open panel steps aside (Opera's rules, each one switchable).
|
||||||
leftPanelWidths: {},
|
leftPanelWidths: {},
|
||||||
leftPanelPinned: {},
|
leftPanelPinned: {}, // retired with the pin button (0.3.86); kept so old files load cleanly
|
||||||
|
// How an open sidebar panel sits next to the page: "overlay" floats over it
|
||||||
|
// and a click in the page closes the panel; "push" moves the page aside and
|
||||||
|
// the panel stays until it is closed.
|
||||||
|
sidebarPanelMode: "overlay",
|
||||||
leftPanelsHidden: [],
|
leftPanelsHidden: [],
|
||||||
sidebarHideOnNewTab: true,
|
sidebarHideOnNewTab: true,
|
||||||
sidebarHideOnRightPanel: true,
|
sidebarHideOnRightPanel: true,
|
||||||
|
|
@ -2528,7 +2532,7 @@ function winOfWebContents(wc) {
|
||||||
const t = tabs.find((x) => x.view.webContents === wc);
|
const t = tabs.find((x) => x.view.webContents === wc);
|
||||||
if (t) return t.w || null;
|
if (t) return t.w || null;
|
||||||
for (const [view, w] of hostOf) if (view.webContents === wc) return windows.includes(w) ? w : null;
|
for (const [view, w] of hostOf) if (view.webContents === wc) return windows.includes(w) ? w : null;
|
||||||
for (const v of [sidebar, quicklinks, quickPanel, leftPanel, panelGrip]) if (v && v.webContents === wc) return primaryW;
|
for (const v of [sidebar, quicklinks, quickPanel, leftPanel, panelGrip, panelHeader]) if (v && v.webContents === wc) return primaryW;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Shared overlays (site info, pickers, downloads, password fill, link pill,
|
// Shared overlays (site info, pickers, downloads, password fill, link pill,
|
||||||
|
|
@ -2620,6 +2624,8 @@ const LEFT_PANEL_W = 400;
|
||||||
const LEFT_PANEL_W_MIN = 280;
|
const LEFT_PANEL_W_MIN = 280;
|
||||||
const TAB_AREA_MIN = 320; // the panel never squeezes the page below this
|
const TAB_AREA_MIN = 320; // the panel never squeezes the page below this
|
||||||
let panelGrip; // panel-grip.html, laid over the panel's right edge
|
let panelGrip; // panel-grip.html, laid over the panel's right edge
|
||||||
|
let panelHeader; // panel-header.html: name, Maximize/Restore, Close over the open panel
|
||||||
|
const PANEL_HEADER_H = 30;
|
||||||
let panelGripDragging = false;
|
let panelGripDragging = false;
|
||||||
const PANEL_GRIP_W = 6;
|
const PANEL_GRIP_W = 6;
|
||||||
// Sidebar width is user-adjustable via a drag grip on the panel's left edge.
|
// Sidebar width is user-adjustable via a drag grip on the panel's left edge.
|
||||||
|
|
@ -3697,6 +3703,11 @@ function layout() {
|
||||||
}
|
}
|
||||||
function layoutHere() {
|
function layoutHere() {
|
||||||
if (!winAlive()) return;
|
if (!winAlive()) return;
|
||||||
|
// A minimized window reports a zero-size content area: laying out then left
|
||||||
|
// every view at 0x0, and nothing laid it out again on restore, so the
|
||||||
|
// window came back blank until it was resized. Keep the last layout; the
|
||||||
|
// "restore" event redoes it.
|
||||||
|
try { if (win.isMinimized()) return; } catch {}
|
||||||
const primary = !curW || curW === primaryW;
|
const primary = !curW || curW === primaryW;
|
||||||
if (fsTabId == null && !userFullscreen) { try { if (win.isFullScreen()) scheduleFullscreenCheck(); } catch {} }
|
if (fsTabId == null && !userFullscreen) { try { if (win.isFullScreen()) scheduleFullscreenCheck(); } catch {} }
|
||||||
const { width, height } = win.getContentBounds();
|
const { width, height } = win.getContentBounds();
|
||||||
|
|
@ -3720,32 +3731,43 @@ function layoutHere() {
|
||||||
// Quick-link panel: a narrow mini-view just right of the strip, showing the
|
// Quick-link panel: a narrow mini-view just right of the strip, showing the
|
||||||
// active quick-link's web app. Only takes space when something is open.
|
// active quick-link's web app. Only takes space when something is open.
|
||||||
const quickW = (primary && quickPanel && activeQuickLinkId && !fsTab) ? leftPanelWidth(QUICK_PANEL_W, width - leftW) : 0;
|
const quickW = (primary && quickPanel && activeQuickLinkId && !fsTab) ? leftPanelWidth(QUICK_PANEL_W, width - leftW) : 0;
|
||||||
if (quickPanel && primary) {
|
|
||||||
quickPanel.setBounds({ x: leftW, y: chromeH, width: quickW, height: bodyH });
|
|
||||||
try { quickPanel.setVisible(quickW > 0); } catch {}
|
|
||||||
}
|
|
||||||
// Sidebar (when visible) claims a fixed slice on the right; the tab views
|
// Sidebar (when visible) claims a fixed slice on the right; the tab views
|
||||||
// shrink to fit alongside it. When hidden, tabs get the full width.
|
// shrink to fit alongside it. When hidden, tabs get the full width.
|
||||||
const sideW = primary && sidebarVisible && !fsTab ? sidebarW : 0;
|
const sideW = primary && sidebarVisible && !fsTab ? sidebarW : 0;
|
||||||
|
// An open panel has its header bar on top. Maximized, it covers the tab
|
||||||
|
// area like a page of its own, up to the right sidebar.
|
||||||
|
const headH = (quickW || (primary && leftPanel && leftPanelId && !fsTab)) ? PANEL_HEADER_H : 0;
|
||||||
|
const quickShownW = quickW && leftPanelMax ? Math.max(quickW, width - leftW - sideW) : quickW;
|
||||||
|
if (quickPanel && primary) {
|
||||||
|
quickPanel.setBounds({ x: leftW, y: chromeH + headH, width: quickShownW, height: Math.max(0, bodyH - headH) });
|
||||||
|
try { quickPanel.setVisible(quickShownW > 0); } catch {}
|
||||||
|
}
|
||||||
// An add-on's left panel. Widened, it covers the tab area like a page of
|
// An add-on's left panel. Widened, it covers the tab area like a page of
|
||||||
// its own, up to the right sidebar, which keeps its width: the tabs keep
|
// its own, up to the right sidebar, which keeps its width: the tabs keep
|
||||||
// their narrow-panel bounds underneath instead of being squeezed aside.
|
// their narrow-panel bounds underneath instead of being squeezed aside.
|
||||||
const addonLeftW = (primary && leftPanel && leftPanelId && !fsTab) ? leftPanelWidth(LEFT_PANEL_W, width - leftW) : 0;
|
const addonLeftW = (primary && leftPanel && leftPanelId && !fsTab) ? leftPanelWidth(LEFT_PANEL_W, width - leftW) : 0;
|
||||||
const addonLeftShownW = addonLeftW && leftPanelMax ? Math.max(addonLeftW, width - leftW - sideW) : addonLeftW;
|
const addonLeftShownW = addonLeftW && leftPanelMax ? Math.max(addonLeftW, width - leftW - sideW) : addonLeftW;
|
||||||
if (leftPanel && primary) {
|
if (leftPanel && primary) {
|
||||||
leftPanel.setBounds({ x: leftW, y: chromeH, width: addonLeftShownW, height: bodyH });
|
leftPanel.setBounds({ x: leftW, y: chromeH + headH, width: addonLeftShownW, height: Math.max(0, bodyH - headH) });
|
||||||
try { leftPanel.setVisible(addonLeftShownW > 0); } catch {}
|
try { leftPanel.setVisible(addonLeftShownW > 0); } catch {}
|
||||||
}
|
}
|
||||||
const panelW = quickW || addonLeftW;
|
const panelW = quickW || addonLeftW;
|
||||||
|
if (panelHeader && primary) {
|
||||||
|
const headW = quickShownW || addonLeftShownW;
|
||||||
|
panelHeader.setBounds({ x: leftW, y: chromeH, width: headW, height: headH });
|
||||||
|
try { panelHeader.setVisible(headH > 0 && headW > 0); } catch {}
|
||||||
|
}
|
||||||
if (panelGrip && primary) {
|
if (panelGrip && primary) {
|
||||||
// At rest a thin strip on the boundary; while dragging, the whole body,
|
// At rest a thin strip on the boundary; while dragging, the whole body,
|
||||||
// so the drag keeps its events wherever the pointer goes.
|
// so the drag keeps its events wherever the pointer goes.
|
||||||
const showGrip = panelW > 0 && !(leftPanelId && leftPanelMax);
|
const showGrip = panelW > 0 && !leftPanelMax;
|
||||||
if (panelGripDragging) panelGrip.setBounds({ x: 0, y: chromeH, width, height: bodyH });
|
if (panelGripDragging) panelGrip.setBounds({ x: 0, y: chromeH, width, height: bodyH });
|
||||||
else panelGrip.setBounds({ x: leftW + panelW - PANEL_GRIP_W / 2, y: chromeH, width: PANEL_GRIP_W, height: bodyH });
|
else panelGrip.setBounds({ x: leftW + panelW - PANEL_GRIP_W / 2, y: chromeH, width: PANEL_GRIP_W, height: bodyH });
|
||||||
try { panelGrip.setVisible(showGrip || panelGripDragging); } catch {}
|
try { panelGrip.setVisible(showGrip || panelGripDragging); } catch {}
|
||||||
}
|
}
|
||||||
const tabX = leftW + panelW;
|
// "push": the page moves aside for the panel. "overlay": the page keeps its
|
||||||
|
// width and the panel floats over its left edge.
|
||||||
|
const tabX = leftW + (settings.sidebarPanelMode === "push" ? panelW : 0);
|
||||||
const tabW = Math.max(0, width - tabX - sideW);
|
const tabW = Math.max(0, width - tabX - sideW);
|
||||||
for (const t of winTabs()) t.view.setBounds({ x: tabX, y: chromeH, width: tabW, height: bodyH });
|
for (const t of winTabs()) t.view.setBounds({ x: tabX, y: chromeH, width: tabW, height: bodyH });
|
||||||
if (sidebar && primary) sidebar.setBounds({ x: tabX + tabW, y: chromeH, width: sideW, height: bodyH });
|
if (sidebar && primary) sidebar.setBounds({ x: tabX + tabW, y: chromeH, width: sideW, height: bodyH });
|
||||||
|
|
@ -4638,6 +4660,8 @@ function createTab(initial, opts = {}) {
|
||||||
tabs.splice(at, 0, tab);
|
tabs.splice(at, 0, tab);
|
||||||
}
|
}
|
||||||
win.contentView.addChildView(view);
|
win.contentView.addChildView(view);
|
||||||
|
// A panel floating over the page stays above the new tab.
|
||||||
|
if (curW === primaryW && leftPanelKey() && settings.sidebarPanelMode !== "push") raisePanelGrip();
|
||||||
wc.on("page-title-updated", (_e, title) => {
|
wc.on("page-title-updated", (_e, title) => {
|
||||||
tab.title = title; emitTabs();
|
tab.title = title; emitTabs();
|
||||||
// Keep the top-of-history title in sync when a page's title loads late.
|
// Keep the top-of-history title in sync when a page's title loads late.
|
||||||
|
|
@ -5218,6 +5242,7 @@ function makeBrowserWindow({ bounds } = {}) {
|
||||||
bw.on("focus", () => focusWin(w));
|
bw.on("focus", () => focusWin(w));
|
||||||
bw.on("blur", closePopupsOnWindowBlur);
|
bw.on("blur", closePopupsOnWindowBlur);
|
||||||
bw.on("resize", layout);
|
bw.on("resize", layout);
|
||||||
|
bw.on("restore", layout);
|
||||||
bw.on("enter-full-screen", layout);
|
bw.on("enter-full-screen", layout);
|
||||||
// Fullscreen left from outside (the OS, or Electron on the page's behalf)
|
// Fullscreen left from outside (the OS, or Electron on the page's behalf)
|
||||||
// while a page still held it: put the toolbar back and tell the page.
|
// while a page still held it: put the toolbar back and tell the page.
|
||||||
|
|
@ -5277,7 +5302,7 @@ function onWindowClose(w) {
|
||||||
if (w === primaryW) primaryW = heir;
|
if (w === primaryW) primaryW = heir;
|
||||||
for (const [view, host] of [...hostOf]) {
|
for (const [view, host] of [...hostOf]) {
|
||||||
if (host !== w) continue;
|
if (host !== w) continue;
|
||||||
const sidePanel = [sidebar, quicklinks, quickPanel, leftPanel, panelGrip].includes(view);
|
const sidePanel = [sidebar, quicklinks, quickPanel, leftPanel, panelGrip, panelHeader].includes(view);
|
||||||
if (!sidePanel) try { view.setVisible(false); } catch {}
|
if (!sidePanel) try { view.setVisible(false); } catch {}
|
||||||
raise(view, heir);
|
raise(view, heir);
|
||||||
}
|
}
|
||||||
|
|
@ -5434,6 +5459,11 @@ function createWindow() {
|
||||||
raise(panelGrip);
|
raise(panelGrip);
|
||||||
panelGrip.webContents.loadFile("panel-grip.html");
|
panelGrip.webContents.loadFile("panel-grip.html");
|
||||||
panelGrip.setVisible(false);
|
panelGrip.setVisible(false);
|
||||||
|
panelHeader = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "panel-header-preload.js") } });
|
||||||
|
try { panelHeader.setBackgroundColor(nativeTheme.shouldUseDarkColors ? "#121826" : "#e4e8ee"); } catch {}
|
||||||
|
raise(panelHeader);
|
||||||
|
deferOverlayLoad(panelHeader, "panel-header.html");
|
||||||
|
panelHeader.setVisible(false);
|
||||||
// Add-on approval overlay (approval-modal capability). Transparent view
|
// Add-on approval overlay (approval-modal capability). Transparent view
|
||||||
// over the tab area, loaded once, shown per request.
|
// over the tab area, loaded once, shown per request.
|
||||||
approvalPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "approval-preload.js") } });
|
approvalPop = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "approval-preload.js") } });
|
||||||
|
|
@ -5480,7 +5510,7 @@ function onLastWindowClosed() {
|
||||||
// pumpApproval (and every later dapp request) until a full restart.
|
// pumpApproval (and every later dapp request) until a full restart.
|
||||||
if (approvalCurrent) { const c = approvalCurrent; approvalCurrent = null; try { c.resolve("cancel"); } catch {} }
|
if (approvalCurrent) { const c = approvalCurrent; approvalCurrent = null; try { c.resolve("cancel"); } catch {} }
|
||||||
for (const q of approvalQueue.splice(0)) { try { q.resolve("cancel"); } catch {} }
|
for (const q of approvalQueue.splice(0)) { try { q.resolve("cancel"); } catch {} }
|
||||||
addressPicker = null; pwFillPop = null; linkStatus = null; sidebar = null; leftPanel = null; panelGrip = null; panelGripDragging = false; approvalPop = null; unlockPop = null; jsDialogPop = null;
|
addressPicker = null; pwFillPop = null; linkStatus = null; sidebar = null; leftPanel = null; panelGrip = null; panelHeader = null; panelGripDragging = false; approvalPop = null; unlockPop = null; jsDialogPop = null;
|
||||||
linkStatusVisible = false;
|
linkStatusVisible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6281,10 +6311,11 @@ function setLeftPanelMax(next) {
|
||||||
// One wide view at a time: a maximized right sidebar steps back, and the
|
// One wide view at a time: a maximized right sidebar steps back, and the
|
||||||
// panel goes on top of tab views created since it opened.
|
// panel goes on top of tab views created since it opened.
|
||||||
if (sidebarMaximized) setSidebarMaximized(false);
|
if (sidebarMaximized) setSidebarMaximized(false);
|
||||||
raise(leftPanel, primaryW);
|
|
||||||
}
|
}
|
||||||
|
if (leftPanelMax) raisePanelGrip();
|
||||||
layout();
|
layout();
|
||||||
try { leftPanel?.webContents.send("sidebar-max-change", leftPanelMax); } catch {}
|
try { leftPanel?.webContents.send("sidebar-max-change", leftPanelMax); } catch {}
|
||||||
|
emitPanelHeader();
|
||||||
return leftPanelMax;
|
return leftPanelMax;
|
||||||
}
|
}
|
||||||
ipcMain.handle("sidebar-maximize", (e) => isLeftSender(e) ? setLeftPanelMax(true) : setSidebarMaximized(true));
|
ipcMain.handle("sidebar-maximize", (e) => isLeftSender(e) ? setLeftPanelMax(true) : setSidebarMaximized(true));
|
||||||
|
|
@ -9000,6 +9031,7 @@ ipcMain.handle("settings-get", () => settings);
|
||||||
ipcMain.handle("settings-set", (_e, key, val) => {
|
ipcMain.handle("settings-set", (_e, key, val) => {
|
||||||
// The default engine must be one that is enabled and not frozen, whichever path sets it.
|
// The default engine must be one that is enabled and not frozen, whichever path sets it.
|
||||||
if (key === "searchEngine" && !enabledEnginesList().some((e) => e.id === val)) return settings;
|
if (key === "searchEngine" && !enabledEnginesList().some((e) => e.id === val)) return settings;
|
||||||
|
if (key === "sidebarPanelMode" && val !== "overlay" && val !== "push") return settings;
|
||||||
if (key in SETTINGS_DEFAULTS) { settings[key] = val; saveSettings(); }
|
if (key in SETTINGS_DEFAULTS) { settings[key] = val; saveSettings(); }
|
||||||
if (key === "webrtcMode") applyWebRTCPolicy();
|
if (key === "webrtcMode") applyWebRTCPolicy();
|
||||||
if (key === "dohMode" || key === "dohProvider" || key === "dohCustom") applyDoh();
|
if (key === "dohMode" || key === "dohProvider" || key === "dohCustom") applyDoh();
|
||||||
|
|
@ -9038,7 +9070,8 @@ ipcMain.handle("settings-set", (_e, key, val) => {
|
||||||
// Quick-links strip: re-layout + push the new state to the strip's view so
|
// Quick-links strip: re-layout + push the new state to the strip's view so
|
||||||
// show/hide and link edits take effect without a relaunch. If the active
|
// show/hide and link edits take effect without a relaunch. If the active
|
||||||
// panel's link was removed, close the panel.
|
// panel's link was removed, close the panel.
|
||||||
if (key === "quickLinksShow" || key === "quickLinks" || key === "leftPanelWidths" || key === "leftPanelPinned" || key === "leftPanelsHidden") {
|
if (key === "sidebarPanelMode") raisePanelGrip();
|
||||||
|
if (key === "quickLinksShow" || key === "quickLinks" || key === "leftPanelWidths" || key === "sidebarPanelMode" || key === "leftPanelsHidden") {
|
||||||
if (activeQuickLinkId && !(settings.quickLinks || []).some((L) => L.id === activeQuickLinkId)) {
|
if (activeQuickLinkId && !(settings.quickLinks || []).some((L) => L.id === activeQuickLinkId)) {
|
||||||
activeQuickLinkId = null;
|
activeQuickLinkId = null;
|
||||||
}
|
}
|
||||||
|
|
@ -9063,13 +9096,52 @@ function quickLinksPayload() {
|
||||||
return {
|
return {
|
||||||
links: settings.quickLinks || [],
|
links: settings.quickLinks || [],
|
||||||
openId: leftPanelId ? "p:" + leftPanelId : activeQuickLinkId,
|
openId: leftPanelId ? "p:" + leftPanelId : activeQuickLinkId,
|
||||||
pinned: leftPanelPinned(),
|
|
||||||
panels: leftPanels().filter((p) => !leftPanelHidden(p.panelId)).map((p) => ({ id: "p:" + p.panelId, title: p.addonName || p.title, icon: p.icon || "" })),
|
panels: leftPanels().filter((p) => !leftPanelHidden(p.panelId)).map((p) => ({ id: "p:" + p.panelId, title: p.addonName || p.title, icon: p.icon || "" })),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function emitQuickLinksState() {
|
function emitQuickLinksState() {
|
||||||
try { quicklinks?.webContents.send("quicklinks-state", quickLinksPayload()); } catch {}
|
try { quicklinks?.webContents.send("quicklinks-state", quickLinksPayload()); } catch {}
|
||||||
|
emitPanelHeader();
|
||||||
}
|
}
|
||||||
|
// ---- the bar over an open panel -------------------------------------------
|
||||||
|
function panelHeaderState() {
|
||||||
|
let title = "";
|
||||||
|
if (activeQuickLinkId) { const L = (settings.quickLinks || []).find((x) => x.id === activeQuickLinkId); title = L ? (L.title || L.url || "") : ""; }
|
||||||
|
else if (leftPanelId) { const p = leftPanels().find((x) => x.panelId === leftPanelId); title = p ? (p.addonName || p.title || "") : ""; }
|
||||||
|
return { title, max: !!leftPanelMax, open: !!leftPanelKey() };
|
||||||
|
}
|
||||||
|
function emitPanelHeader() {
|
||||||
|
try { if (panelHeader && overlayLoaded(panelHeader)) panelHeader.webContents.send("panel-header-state", panelHeaderState()); } catch {}
|
||||||
|
}
|
||||||
|
const isHeaderSender = (e) => !!panelHeader && e.sender === panelHeader.webContents;
|
||||||
|
ipcMain.handle("panel-header-state", (e) => isHeaderSender(e) ? panelHeaderState() : null);
|
||||||
|
ipcMain.handle("panel-header-max", (e) => isHeaderSender(e) && leftPanelKey() ? setLeftPanelMax(!leftPanelMax) : false);
|
||||||
|
ipcMain.handle("panel-header-close", (e) => { if (isHeaderSender(e)) hideLeftPanels(); return true; });
|
||||||
|
// The gear above "+" on the strip: how panels sit next to the page, and the
|
||||||
|
// Sidebar settings. A native menu, so it can open past the strip's edge.
|
||||||
|
ipcMain.handle("quicklinks-menu", (e, rect) => {
|
||||||
|
if (!quicklinks || e.sender !== quicklinks.webContents || !primaryW?.win || primaryW.win.isDestroyed()) return false;
|
||||||
|
const setMode = (mode) => {
|
||||||
|
if (settings.sidebarPanelMode === mode) return;
|
||||||
|
settings.sidebarPanelMode = mode;
|
||||||
|
saveSettings();
|
||||||
|
raisePanelGrip();
|
||||||
|
layout();
|
||||||
|
broadcastSettings();
|
||||||
|
};
|
||||||
|
const push = settings.sidebarPanelMode === "push";
|
||||||
|
const menu = Menu.buildFromTemplate([
|
||||||
|
{ label: "Panels open over the page", type: "radio", checked: !push, click: () => setMode("overlay") },
|
||||||
|
{ label: "Panels push the page aside", type: "radio", checked: push, click: () => setMode("push") },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ label: "Reset panel widths", enabled: !!(settings.leftPanelWidths && Object.keys(settings.leftPanelWidths).length), click: () => { settings.leftPanelWidths = {}; saveSettings(); layout(); } },
|
||||||
|
{ label: "Sidebar settings…", click: () => openSettingsTab("sidebar") },
|
||||||
|
]);
|
||||||
|
const r = rect || {};
|
||||||
|
const y = Math.round(Number(r.y) || 0) + (primaryW.win === win ? CHROME_H : primaryW.chromeH || CHROME_H);
|
||||||
|
menu.popup({ window: primaryW.win, x: Math.round((Number(r.x) || 0) + (Number(r.width) || 0)), y });
|
||||||
|
return true;
|
||||||
|
});
|
||||||
function leftPanelHidden(panelId) {
|
function leftPanelHidden(panelId) {
|
||||||
return Array.isArray(settings.leftPanelsHidden) && settings.leftPanelsHidden.includes("p:" + panelId);
|
return Array.isArray(settings.leftPanelsHidden) && settings.leftPanelsHidden.includes("p:" + panelId);
|
||||||
}
|
}
|
||||||
|
|
@ -9089,34 +9161,22 @@ function leftPanelWidth(def, room) {
|
||||||
const max = Math.max(LEFT_PANEL_W_MIN, (room || 0) - TAB_AREA_MIN);
|
const max = Math.max(LEFT_PANEL_W_MIN, (room || 0) - TAB_AREA_MIN);
|
||||||
return Math.max(LEFT_PANEL_W_MIN, Math.min(max, saved || def));
|
return Math.max(LEFT_PANEL_W_MIN, Math.min(max, saved || def));
|
||||||
}
|
}
|
||||||
// The grip has to stay above whichever panel view was raised last.
|
// The open panel, its header and the grip go on top of the tab views (over
|
||||||
|
// the page in "overlay" mode, and above tabs created since it opened).
|
||||||
function raisePanelGrip() {
|
function raisePanelGrip() {
|
||||||
if (!panelGrip) return;
|
if (activeQuickLinkId && quickPanel) raise(quickPanel, primaryW);
|
||||||
raise(panelGrip, primaryW);
|
if (leftPanelId && leftPanel) raise(leftPanel, primaryW);
|
||||||
|
if (panelHeader && leftPanelKey()) { loadOverlay(panelHeader); raise(panelHeader, primaryW); }
|
||||||
|
if (panelGrip) raise(panelGrip, primaryW);
|
||||||
}
|
}
|
||||||
// Opera-style: the left panel steps aside when the user opens something else
|
// Opera-style: the left panel steps aside when the user opens something else
|
||||||
// (a new tab, the right sidebar). It is only hidden — the app keeps its
|
// (a new tab, the right sidebar). It is only hidden — the app keeps its
|
||||||
// state, so reopening it is instant and lands where the user left off.
|
// state, so reopening it is instant and lands where the user left off.
|
||||||
// Opera's other rule: a click in the page closes the panel, unless the user
|
// Opera's other rule: a panel floating over the page closes when the page is
|
||||||
// pinned that app (settings.leftPanelPinned, keyed like leftPanelWidths).
|
// clicked. A panel that pushed the page aside stays until it is closed.
|
||||||
function leftPanelPinned() {
|
|
||||||
const k = leftPanelKey();
|
|
||||||
return !!(k && settings.leftPanelPinned && settings.leftPanelPinned[k]);
|
|
||||||
}
|
|
||||||
ipcMain.handle("quicklinks-pin", (e, on) => {
|
|
||||||
if (!quicklinks || e.sender !== quicklinks.webContents) return false;
|
|
||||||
const k = leftPanelKey();
|
|
||||||
if (!k) return false;
|
|
||||||
const next = { ...(settings.leftPanelPinned || {}) };
|
|
||||||
if (on) next[k] = true; else delete next[k];
|
|
||||||
settings.leftPanelPinned = next;
|
|
||||||
saveSettings();
|
|
||||||
emitQuickLinksState();
|
|
||||||
return !!on;
|
|
||||||
});
|
|
||||||
// home-preload reports a pointerdown in a web tab's page.
|
// home-preload reports a pointerdown in a web tab's page.
|
||||||
ipcMain.on("tab-pointerdown", (e) => {
|
ipcMain.on("tab-pointerdown", (e) => {
|
||||||
if (settings.sidebarHideOnPageClick === false || !leftPanelKey() || leftPanelPinned()) return;
|
if (settings.sidebarPanelMode === "push" || settings.sidebarHideOnPageClick === false || !leftPanelKey()) return;
|
||||||
const t = tabs.find((x) => x.view?.webContents === e.sender);
|
const t = tabs.find((x) => x.view?.webContents === e.sender);
|
||||||
if (!t || !isActiveId(t.id)) return;
|
if (!t || !isActiveId(t.id)) return;
|
||||||
hideLeftPanels();
|
hideLeftPanels();
|
||||||
|
|
@ -9186,6 +9246,7 @@ function openLeftPanel(panelId) {
|
||||||
function closeLeftPanel() {
|
function closeLeftPanel() {
|
||||||
if (!leftPanelId) return;
|
if (!leftPanelId) return;
|
||||||
leftPanelId = null;
|
leftPanelId = null;
|
||||||
|
if (leftPanelMax) { leftPanelMax = false; try { leftPanel?.webContents.send("sidebar-max-change", false); } catch {} }
|
||||||
layout();
|
layout();
|
||||||
emitQuickLinksState();
|
emitQuickLinksState();
|
||||||
try { leftPanel?.webContents.send("sidebar-visibility", false); } catch {}
|
try { leftPanel?.webContents.send("sidebar-visibility", false); } catch {}
|
||||||
|
|
@ -9194,6 +9255,7 @@ function openQuickPanel(id) {
|
||||||
const L = (settings.quickLinks || []).find((x) => x.id === id);
|
const L = (settings.quickLinks || []).find((x) => x.id === id);
|
||||||
if (!quickPanel || !L || !L.url) return;
|
if (!quickPanel || !L || !L.url) return;
|
||||||
if (leftPanelId) closeLeftPanel();
|
if (leftPanelId) closeLeftPanel();
|
||||||
|
if (activeQuickLinkId !== id) leftPanelMax = false;
|
||||||
activeQuickLinkId = id;
|
activeQuickLinkId = id;
|
||||||
try {
|
try {
|
||||||
const cur = quickPanel.webContents.getURL();
|
const cur = quickPanel.webContents.getURL();
|
||||||
|
|
@ -9209,6 +9271,7 @@ function openQuickPanel(id) {
|
||||||
}
|
}
|
||||||
function closeQuickPanel() {
|
function closeQuickPanel() {
|
||||||
activeQuickLinkId = null;
|
activeQuickLinkId = null;
|
||||||
|
if (leftPanelMax && !leftPanelId) leftPanelMax = false;
|
||||||
layout();
|
layout();
|
||||||
emitQuickLinksState();
|
emitQuickLinksState();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,8 @@
|
||||||
"quicklinks.html",
|
"quicklinks.html",
|
||||||
"quicklinks-preload.js",
|
"quicklinks-preload.js",
|
||||||
"panel-grip-preload.js",
|
"panel-grip-preload.js",
|
||||||
|
"panel-header.html",
|
||||||
|
"panel-header-preload.js",
|
||||||
"panel-grip.html",
|
"panel-grip.html",
|
||||||
"engine-icons/**/*",
|
"engine-icons/**/*",
|
||||||
"lib/**/*",
|
"lib/**/*",
|
||||||
|
|
|
||||||
8
panel-header-preload.js
Normal file
8
panel-header-preload.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// Preload for panel-header.html, the bar over an open sidebar panel.
|
||||||
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
contextBridge.exposeInMainWorld("panelHeader", {
|
||||||
|
getState: () => ipcRenderer.invoke("panel-header-state"),
|
||||||
|
toggleMax: () => ipcRenderer.invoke("panel-header-max"),
|
||||||
|
close: () => ipcRenderer.invoke("panel-header-close"),
|
||||||
|
onState: (cb) => ipcRenderer.on("panel-header-state", (_e, s) => cb(s)),
|
||||||
|
});
|
||||||
48
panel-header.html
Normal file
48
panel-header.html
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html><head><meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
/* The bar over an open sidebar panel (a web app or an extension's panel):
|
||||||
|
its name, then Maximize/Restore and Close at the top right. */
|
||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
--bg:#121826; --ink:#e7eaf1; --mut:#8b98a9; --line2:rgba(255,255,255,.10);
|
||||||
|
--acid:#d6ff3d; --hover: rgba(255,255,255,.08);
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root { --bg:#e4e8ee; --ink:#1a1f28; --mut:#5c6676; --line2:rgba(0,0,0,.10); --acid:#0AC18E; --hover: rgba(0,0,0,.06); }
|
||||||
|
}
|
||||||
|
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink); overflow: hidden; user-select: none;
|
||||||
|
font: 600 12.5px/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
|
||||||
|
.bar { display: flex; align-items: center; height: 100%; padding: 0 4px 0 12px; box-sizing: border-box;
|
||||||
|
border-bottom: 1px solid var(--line2); gap: 2px; }
|
||||||
|
.title { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
button { width: 26px; height: 24px; border: none; border-radius: 6px; background: transparent; color: var(--mut);
|
||||||
|
display: grid; place-items: center; cursor: pointer; padding: 0; }
|
||||||
|
button:hover { background: var(--hover); color: var(--ink); }
|
||||||
|
button svg { width: 14px; height: 14px; display: block; }
|
||||||
|
</style></head>
|
||||||
|
<body>
|
||||||
|
<div class="bar">
|
||||||
|
<span class="title" id="title"></span>
|
||||||
|
<button id="max" title="Maximize"></button>
|
||||||
|
<button id="close" title="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const H = window.panelHeader;
|
||||||
|
const ICON_MAX = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2"/></svg>';
|
||||||
|
const ICON_RESTORE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="8" width="12" height="12" rx="2"/><path d="M8 8V6a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2"/></svg>';
|
||||||
|
const title = document.getElementById("title"), max = document.getElementById("max");
|
||||||
|
function render(s) {
|
||||||
|
title.textContent = (s && s.title) || "";
|
||||||
|
const isMax = !!(s && s.max);
|
||||||
|
max.innerHTML = isMax ? ICON_RESTORE : ICON_MAX;
|
||||||
|
max.title = isMax ? "Restore" : "Maximize";
|
||||||
|
}
|
||||||
|
max.onclick = () => H && H.toggleMax();
|
||||||
|
document.getElementById("close").onclick = () => H && H.close();
|
||||||
|
// Double-click the bar: maximize / restore, like a window title bar.
|
||||||
|
document.querySelector(".bar").addEventListener("dblclick", (e) => { if (e.target.closest("button")) return; H && H.toggleMax(); });
|
||||||
|
H && H.getState().then(render).catch(() => {});
|
||||||
|
H && H.onState(render);
|
||||||
|
</script>
|
||||||
|
</body></html>
|
||||||
|
|
@ -3,7 +3,8 @@ contextBridge.exposeInMainWorld("quicklinks", {
|
||||||
getState: () => ipcRenderer.invoke("quicklinks-get-state"),
|
getState: () => ipcRenderer.invoke("quicklinks-get-state"),
|
||||||
open: (id) => ipcRenderer.invoke("quicklinks-open", String(id || "")),
|
open: (id) => ipcRenderer.invoke("quicklinks-open", String(id || "")),
|
||||||
close: () => ipcRenderer.invoke("quicklinks-close"),
|
close: () => ipcRenderer.invoke("quicklinks-close"),
|
||||||
pin: (on) => ipcRenderer.invoke("quicklinks-pin", !!on),
|
// The gear: rect is the button's box in the strip, for placing the menu.
|
||||||
|
menu: (rect) => ipcRenderer.invoke("quicklinks-menu", rect),
|
||||||
add: () => ipcRenderer.invoke("quicklinks-add"),
|
add: () => ipcRenderer.invoke("quicklinks-add"),
|
||||||
onState: (cb) => ipcRenderer.on("quicklinks-state", (_e, d) => cb(d)),
|
onState: (cb) => ipcRenderer.on("quicklinks-state", (_e, d) => cb(d)),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -39,19 +39,17 @@
|
||||||
.sep { width: 22px; height: 1px; background: var(--line2); margin: 4px 0; }
|
.sep { width: 22px; height: 1px; background: var(--line2); margin: 4px 0; }
|
||||||
.icon.emoji { font-size: 18px; }
|
.icon.emoji { font-size: 18px; }
|
||||||
.add:hover { color: var(--acid); }
|
.add:hover { color: var(--acid); }
|
||||||
/* Pin: keeps the open panel when the page is clicked. Shown only while a
|
/* Gear: how panels sit next to the page, and the Sidebar settings. */
|
||||||
panel is open; lit when that app is pinned. */
|
.gear { color: var(--mut); margin-bottom: 2px; }
|
||||||
.pin { color: var(--mut); margin-bottom: 4px; }
|
.gear:hover { color: var(--ink); }
|
||||||
.pin[hidden] { display: none; }
|
.gear svg { width: 17px; height: 17px; }
|
||||||
.pin.on { color: var(--acid); background: var(--acid-tint); }
|
|
||||||
.pin svg { width: 16px; height: 16px; }
|
|
||||||
</style></head>
|
</style></head>
|
||||||
<body>
|
<body>
|
||||||
<div class="strip">
|
<div class="strip">
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="icons" id="icons"></div>
|
<div class="icons" id="icons"></div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<button class="icon pin" id="pinBtn" hidden><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4h6l-1 6 3 3v2H7v-2l3-3-1-6z"/><path d="M12 15v6"/></svg><span class="tt" id="pinTt">Keep open</span></button>
|
<button class="icon gear" id="gearBtn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg><span class="tt">Sidebar options</span></button>
|
||||||
<button class="icon add" id="addBtn" title="Add link">+<span class="tt">Add link</span></button>
|
<button class="icon add" id="addBtn" title="Add link">+<span class="tt">Add link</span></button>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -59,10 +57,11 @@
|
||||||
const icons = document.getElementById("icons");
|
const icons = document.getElementById("icons");
|
||||||
const addBtn = document.getElementById("addBtn");
|
const addBtn = document.getElementById("addBtn");
|
||||||
addBtn.onclick = () => Q && Q.add();
|
addBtn.onclick = () => Q && Q.add();
|
||||||
const pinBtn = document.getElementById("pinBtn");
|
const gearBtn = document.getElementById("gearBtn");
|
||||||
const pinTt = document.getElementById("pinTt");
|
gearBtn.onclick = () => {
|
||||||
let pinned = false;
|
const r = gearBtn.getBoundingClientRect();
|
||||||
pinBtn.onclick = () => Q && Q.pin && Q.pin(!pinned);
|
Q && Q.menu && Q.menu({ x: r.left, y: r.top, width: r.width, height: r.height });
|
||||||
|
};
|
||||||
// Bundled brand marks. Each SVG is tight (20×20 viewBox, no padding), the
|
// Bundled brand marks. Each SVG is tight (20×20 viewBox, no padding), the
|
||||||
// colour is baked in so icons read at a glance in both themes. Unknown ids
|
// colour is baked in so icons read at a glance in both themes. Unknown ids
|
||||||
// fall back to a letter chip. All marks are purely to identify services.
|
// fall back to a letter chip. All marks are purely to identify services.
|
||||||
|
|
@ -80,10 +79,6 @@
|
||||||
function render(data) {
|
function render(data) {
|
||||||
const links = (data && data.links) || [];
|
const links = (data && data.links) || [];
|
||||||
const openId = (data && data.openId) || null;
|
const openId = (data && data.openId) || null;
|
||||||
pinned = !!(data && data.pinned);
|
|
||||||
pinBtn.hidden = !openId;
|
|
||||||
pinBtn.classList.toggle("on", pinned);
|
|
||||||
pinTt.textContent = pinned ? "Pinned: stays open when you click the page" : "Keep open when you click the page";
|
|
||||||
icons.innerHTML = "";
|
icons.innerHTML = "";
|
||||||
// Extension panels (registered with side: "left") come first.
|
// Extension panels (registered with side: "left") come first.
|
||||||
const panels = (data && data.panels) || [];
|
const panels = (data && data.panels) || [];
|
||||||
|
|
|
||||||
|
|
@ -939,11 +939,19 @@
|
||||||
</section>
|
</section>
|
||||||
<section id="sidebar" hidden>
|
<section id="sidebar" hidden>
|
||||||
<h1>Sidebar</h1>
|
<h1>Sidebar</h1>
|
||||||
<p class="lede">The strip on the left edge: web apps that open in a panel beside the page, and extension panels. Drag a panel's right edge to resize it (double-click the edge to reset). The pin keeps a panel open when you click the page.</p>
|
<p class="lede">The strip on the left edge: web apps that open in a panel beside the page, and extension panels. Drag a panel's right edge to resize it (double-click the edge to reset). The bar over an open panel maximizes it over the page, restores it, or closes it.</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="txt"><div class="t">Show the strip</div><div class="d">A thin vertical column on the left side of every page.</div></div>
|
<div class="txt"><div class="t">Show the strip</div><div class="d">A thin vertical column on the left side of every page.</div></div>
|
||||||
<label class="sw"><input type="checkbox" id="quickLinksShow"><span class="track"><span class="knob"></span></span></label>
|
<label class="sw"><input type="checkbox" id="quickLinksShow"><span class="track"><span class="knob"></span></span></label>
|
||||||
</div>
|
</div>
|
||||||
|
<h2 class="sub">How panels open</h2>
|
||||||
|
<p class="subd">Also in the gear on the strip, above +.</p>
|
||||||
|
<div class="row" style="flex-direction:column;align-items:stretch;gap:10px">
|
||||||
|
<div style="display:flex;flex-direction:column;gap:6px">
|
||||||
|
<label class="polrow"><input type="radio" name="sidebarPanelMode" value="overlay"><span><b>Over the page</b> <span class="pmuted">— the panel floats over the left of the page, which keeps its width. A click in the page closes it.</span></span></label>
|
||||||
|
<label class="polrow"><input type="radio" name="sidebarPanelMode" value="push"><span><b>Push the page aside</b> <span class="pmuted">— the page narrows to make room, and the panel stays open until you close it.</span></span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<h2 class="sub">Web apps</h2>
|
<h2 class="sub">Web apps</h2>
|
||||||
<p class="subd">Sites that open in the panel. A site you installed as an app can be added from Apps.</p>
|
<p class="subd">Sites that open in the panel. A site you installed as an app can be added from Apps.</p>
|
||||||
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
|
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">
|
||||||
|
|
@ -970,7 +978,7 @@
|
||||||
<label class="sw"><input type="checkbox" id="sidebarHideOnRightPanel"><span class="track"><span class="knob"></span></span></label>
|
<label class="sw"><input type="checkbox" id="sidebarHideOnRightPanel"><span class="track"><span class="knob"></span></span></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="txt"><div class="t">You click the page</div><div class="d">Pinned panels stay open.</div></div>
|
<div class="txt"><div class="t">You click the page</div><div class="d">Panels that open over the page. A panel that pushes the page aside stays open.</div></div>
|
||||||
<label class="sw"><input type="checkbox" id="sidebarHideOnPageClick"><span class="track"><span class="knob"></span></span></label>
|
<label class="sw"><input type="checkbox" id="sidebarHideOnPageClick"><span class="track"><span class="knob"></span></span></label>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -1291,12 +1299,11 @@
|
||||||
const addBtn = document.getElementById("qlAdd");
|
const addBtn = document.getElementById("qlAdd");
|
||||||
if (!list || !addBtn) return;
|
if (!list || !addBtn) return;
|
||||||
const esc = (v) => String(v || "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">","\"":""","'":"'" })[c]);
|
const esc = (v) => String(v || "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">","\"":""","'":"'" })[c]);
|
||||||
// A row's pin and width share their key with the strip: "q:<id>".
|
// A row's width shares its key with the strip: "q:<id>".
|
||||||
const pinBtnHtml = (on, key) => `<button class="btn ghost" data-pin="${esc(key)}" title="${on ? "Pinned: stays open when you click the page" : "Not pinned: closes when you click the page"}" style="${on ? "color:var(--acid-text)" : ""}">${on ? "Pinned" : "Pin"}</button>`;
|
|
||||||
const widthBtnHtml = (w, key) => w ? `<button class="btn ghost" data-width="${esc(key)}" title="Panel width ${w} px">Reset width</button>` : "";
|
const widthBtnHtml = (w, key) => w ? `<button class="btn ghost" data-width="${esc(key)}" title="Panel width ${w} px">Reset width</button>` : "";
|
||||||
let cur = s;
|
let cur = s;
|
||||||
function render(links) {
|
function render(links) {
|
||||||
const pinned = cur.leftPanelPinned || {}, widths = cur.leftPanelWidths || {};
|
const widths = cur.leftPanelWidths || {};
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
if (!links.length) {
|
if (!links.length) {
|
||||||
const empty = document.createElement("div");
|
const empty = document.createElement("div");
|
||||||
|
|
@ -1314,7 +1321,7 @@
|
||||||
row.style.cssText = "display:flex;align-items:center;gap:8px;flex-wrap:wrap;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:8px 12px";
|
row.style.cssText = "display:flex;align-items:center;gap:8px;flex-wrap:wrap;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:8px 12px";
|
||||||
row.innerHTML = `<span class="grip" title="Drag to reorder">⠿</span><div style="width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,.06);display:grid;place-items:center;font-weight:600;color:var(--acid-text);flex:none">${esc((L.title || "?").slice(0,1).toUpperCase())}</div>
|
row.innerHTML = `<span class="grip" title="Drag to reorder">⠿</span><div style="width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,.06);display:grid;place-items:center;font-weight:600;color:var(--acid-text);flex:none">${esc((L.title || "?").slice(0,1).toUpperCase())}</div>
|
||||||
<div style="flex:1;min-width:140px"><div style="font-weight:600">${esc(L.title || L.url)}</div><div style="font-size:12px;color:var(--mut);word-break:break-all">${esc(L.url)}${widths[key] ? " · " + Number(widths[key]) + " px" : ""}</div></div>
|
<div style="flex:1;min-width:140px"><div style="font-weight:600">${esc(L.title || L.url)}</div><div style="font-size:12px;color:var(--mut);word-break:break-all">${esc(L.url)}${widths[key] ? " · " + Number(widths[key]) + " px" : ""}</div></div>
|
||||||
${pinBtnHtml(!!pinned[key], key)}${widthBtnHtml(widths[key], key)}
|
${widthBtnHtml(widths[key], key)}
|
||||||
<button class="btn ghost" data-del="${esc(L.id)}">Remove</button>`;
|
<button class="btn ghost" data-del="${esc(L.id)}">Remove</button>`;
|
||||||
list.appendChild(row);
|
list.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
@ -1340,23 +1347,18 @@
|
||||||
C.set("quickLinks", now);
|
C.set("quickLinks", now);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
wirePinWidth(list);
|
wireWidth(list);
|
||||||
}
|
}
|
||||||
// Shared with the extension-panel list below.
|
// Shared with the extension-panel list below.
|
||||||
window.__sbWirePinWidth = wirePinWidth;
|
window.__sbWireWidth = wireWidth;
|
||||||
function wirePinWidth(root) {
|
function wireWidth(root) {
|
||||||
root.querySelectorAll("[data-pin]").forEach((b) => b.onclick = async () => {
|
|
||||||
const p = { ...((await C.get()).leftPanelPinned || {}) };
|
|
||||||
if (p[b.dataset.pin]) delete p[b.dataset.pin]; else p[b.dataset.pin] = true;
|
|
||||||
C.set("leftPanelPinned", p);
|
|
||||||
});
|
|
||||||
root.querySelectorAll("[data-width]").forEach((b) => b.onclick = async () => {
|
root.querySelectorAll("[data-width]").forEach((b) => b.onclick = async () => {
|
||||||
const w = { ...((await C.get()).leftPanelWidths || {}) };
|
const w = { ...((await C.get()).leftPanelWidths || {}) };
|
||||||
delete w[b.dataset.width];
|
delete w[b.dataset.width];
|
||||||
C.set("leftPanelWidths", w);
|
C.set("leftPanelWidths", w);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
window.__sbButtons = { pinBtnHtml, widthBtnHtml };
|
window.__sbButtons = { widthBtnHtml };
|
||||||
render(s.quickLinks || []);
|
render(s.quickLinks || []);
|
||||||
addBtn.onclick = async () => {
|
addBtn.onclick = async () => {
|
||||||
const t = String(titleIn.value || "").trim();
|
const t = String(titleIn.value || "").trim();
|
||||||
|
|
@ -1381,7 +1383,7 @@
|
||||||
let cur = s;
|
let cur = s;
|
||||||
async function render() {
|
async function render() {
|
||||||
let panels = []; try { panels = (await C.sidebarLeftPanels()) || []; } catch {}
|
let panels = []; try { panels = (await C.sidebarLeftPanels()) || []; } catch {}
|
||||||
const pinned = cur.leftPanelPinned || {}, widths = cur.leftPanelWidths || {};
|
const widths = cur.leftPanelWidths || {};
|
||||||
const B = window.__sbButtons;
|
const B = window.__sbButtons;
|
||||||
if (!panels.length) { box.innerHTML = `<div class="pmuted" style="font-size:12.5px">No extension opens from the strip.</div>`; return; }
|
if (!panels.length) { box.innerHTML = `<div class="pmuted" style="font-size:12.5px">No extension opens from the strip.</div>`; return; }
|
||||||
box.innerHTML = panels.map((p) => {
|
box.innerHTML = panels.map((p) => {
|
||||||
|
|
@ -1389,7 +1391,7 @@
|
||||||
return `<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:8px 12px">
|
return `<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:8px 12px">
|
||||||
<div style="width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,.06);display:grid;place-items:center;flex:none">${icon}</div>
|
<div style="width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,.06);display:grid;place-items:center;flex:none">${icon}</div>
|
||||||
<div style="flex:1;min-width:140px"><div style="font-weight:600">${esc(p.title)}</div><div style="font-size:12px;color:var(--mut)">${p.hidden ? "Not on the strip" : "On the strip"}${widths[p.id] ? " · " + Number(widths[p.id]) + " px" : ""}</div></div>
|
<div style="flex:1;min-width:140px"><div style="font-weight:600">${esc(p.title)}</div><div style="font-size:12px;color:var(--mut)">${p.hidden ? "Not on the strip" : "On the strip"}${widths[p.id] ? " · " + Number(widths[p.id]) + " px" : ""}</div></div>
|
||||||
${B ? B.pinBtnHtml(!!pinned[p.id], p.id) + B.widthBtnHtml(widths[p.id], p.id) : ""}
|
${B ? B.widthBtnHtml(widths[p.id], p.id) : ""}
|
||||||
<button class="btn ghost" data-show="${esc(p.id)}">${p.hidden ? "Show" : "Hide"}</button></div>`;
|
<button class="btn ghost" data-show="${esc(p.id)}">${p.hidden ? "Show" : "Hide"}</button></div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
box.querySelectorAll("[data-show]").forEach((b) => b.onclick = async () => {
|
box.querySelectorAll("[data-show]").forEach((b) => b.onclick = async () => {
|
||||||
|
|
@ -1397,7 +1399,7 @@
|
||||||
if (h.has(b.dataset.show)) h.delete(b.dataset.show); else h.add(b.dataset.show);
|
if (h.has(b.dataset.show)) h.delete(b.dataset.show); else h.add(b.dataset.show);
|
||||||
C.set("leftPanelsHidden", [...h]);
|
C.set("leftPanelsHidden", [...h]);
|
||||||
});
|
});
|
||||||
if (window.__sbWirePinWidth) window.__sbWirePinWidth(box);
|
if (window.__sbWireWidth) window.__sbWireWidth(box);
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
document.addEventListener("section", (e) => { if (String(e.detail) === "sidebar") render(); });
|
document.addEventListener("section", (e) => { if (String(e.detail) === "sidebar") render(); });
|
||||||
|
|
@ -1991,6 +1993,13 @@
|
||||||
// The active radio reflects the current setting; changing it just calls
|
// The active radio reflects the current setting; changing it just calls
|
||||||
// C.set — the F12 handler in main.js reads settings.devToolsDock each
|
// C.set — the F12 handler in main.js reads settings.devToolsDock each
|
||||||
// time DevTools opens, so no restart is needed for the choice to apply.
|
// time DevTools opens, so no restart is needed for the choice to apply.
|
||||||
|
// Sidebar › How panels open. The gear on the strip changes it too.
|
||||||
|
{
|
||||||
|
const sync = (mode) => document.querySelectorAll('input[name="sidebarPanelMode"]').forEach((r) => { r.checked = (r.value === (mode === "push" ? "push" : "overlay")); });
|
||||||
|
sync(s.sidebarPanelMode);
|
||||||
|
document.querySelectorAll('input[name="sidebarPanelMode"]').forEach((r) => r.addEventListener("change", () => { if (r.checked) C.set("sidebarPanelMode", r.value); }));
|
||||||
|
if (C.onSettingsUpdate) C.onSettingsUpdate((next) => sync(next.sidebarPanelMode));
|
||||||
|
}
|
||||||
const devToolsDock = s.devToolsDock || "bottom";
|
const devToolsDock = s.devToolsDock || "bottom";
|
||||||
document.querySelectorAll('input[name="devToolsDock"]').forEach((r) => {
|
document.querySelectorAll('input[name="devToolsDock"]').forEach((r) => {
|
||||||
r.checked = (r.value === devToolsDock);
|
r.checked = (r.value === devToolsDock);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue