diff --git a/main.js b/main.js
index e0da04d..d374b82 100644
--- a/main.js
+++ b/main.js
@@ -83,7 +83,7 @@ const SEARCH_ENGINES = {
};
// Engines enabled by default (shown in the toolbar dropdown). The rest are in the
// catalog and can be turned on from Settings. Custom + detected engines are always on.
-const DEFAULT_ENABLED = ["duckduckgo", "google", "brave", "bing", "startpage"];
+const DEFAULT_ENABLED = ["startpage", "duckduckgo", "google", "brave", "bing"];
// DuckDuckGo's icon service reliably returns a favicon for ANY domain from one
// privacy-respecting host — far more robust than guessing /favicon.ico per site.
const faviconUrl = (domain) => (domain ? `https://icons.duckduckgo.com/ip3/${domain}.ico` : null);
@@ -203,7 +203,7 @@ const SETTINGS_DEFAULTS = {
timezoneMode: "show", timezoneValue: "Europe/Berlin", // IANA zone for manual
languageMode: "show", languageSpoof: "en-US", languageValue: "en-US", // spoof = top-10 pick, manual = free text
locationMode: "hide", locationRegion: "europe", locationLat: "40.7128", locationLon: "-74.0060", // spoof by region, or manual coords
- searchEngine: "duckduckgo",// default search engine (built-in id or a custom id)
+ searchEngine: "startpage",// default search engine (built-in id or a custom id)
installedEngines: DEFAULT_ENABLED.slice(), // built-in engines added to the user's list (visible in Settings)
enabledEngines: DEFAULT_ENABLED.slice(), // subset that's currently toggled on (shown in the toolbar dropdown)
engineOrder: [], // user-defined display order of engine ids (empty = natural)
@@ -1759,7 +1759,14 @@ function closeTab(id) {
}
function createWindow() {
- win = new BrowserWindow({ width: 1220, height: 840, title: "Theseus Navigator", backgroundColor: "#0f1420" });
+ win = new BrowserWindow({
+ width: 1220, height: 840, title: "Theseus Navigator", backgroundColor: "#0f1420",
+ // Taskbar / titlebar icon. Packaged builds ship build/icon.ico as
+ // extraResource; dev reads the source file directly.
+ icon: app.isPackaged
+ ? path.join(process.resourcesPath, "icon.ico")
+ : path.join(__dirname, "build", "icon.ico"),
+ });
chrome = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "preload.js") } });
win.contentView.addChildView(chrome);
chrome.webContents.loadFile("chrome.html");
diff --git a/nsis/make-icons.mjs b/nsis/make-icons.mjs
new file mode 100644
index 0000000..4d29618
--- /dev/null
+++ b/nsis/make-icons.mjs
@@ -0,0 +1,48 @@
+// One-shot script: render site-theseus-x/assets/favicon.svg to the icon
+// files electron-builder wants — a 512x512 PNG (main icon) and a multi-
+// resolution .ico (Windows taskbar / installer / shortcut icon).
+//
+// Not in the runtime app bundle; run manually when the brand changes:
+// node nsis/make-icons.mjs
+// Outputs build/icon.png and build/icon.ico (both gitignored regenerated).
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import sharp from "sharp";
+import pngToIco from "png-to-ico";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+// SVG source lives in the theseus.x standalone site so the browser icon
+// and the site brand stay in lockstep.
+const SVG = path.join(__dirname, "..", "..", "site-theseus-x", "assets", "favicon.svg");
+// Icons drop into build/, which is gitignored (regenerated artifacts).
+// electron-builder picks them up from there via package.json.
+const OUT_DIR = path.join(__dirname, "..", "build");
+const PNG_512 = path.join(OUT_DIR, "icon.png");
+const ICO = path.join(OUT_DIR, "icon.ico");
+const TMP_DIR = path.join(OUT_DIR, ".icon-tmp");
+const ICO_SIZES = [16, 24, 32, 48, 64, 128, 256];
+
+if (!fs.existsSync(SVG)) throw new Error("SVG not found at " + SVG);
+console.log("source:", SVG);
+
+// Main 512x512 PNG for macOS / linux / electron-builder auto-derived Windows.
+await sharp(SVG).resize(512, 512).png().toFile(PNG_512);
+console.log("wrote", PNG_512);
+
+// Write individual sized PNGs to a temp dir, then feed their paths to
+// png-to-ico so it preserves each as a distinct image inside the .ico.
+fs.mkdirSync(TMP_DIR, { recursive: true });
+const paths = [];
+for (const size of ICO_SIZES) {
+ const p = path.join(TMP_DIR, `icon-${size}.png`);
+ await sharp(SVG).resize(size, size).png().toFile(p);
+ paths.push(p);
+}
+const ico = await pngToIco(paths);
+fs.writeFileSync(ICO, ico);
+console.log("wrote", ICO, `(${ICO_SIZES.length} sizes)`);
+
+// Clean up the temp PNGs.
+for (const p of paths) fs.unlinkSync(p);
+fs.rmdirSync(TMP_DIR);
diff --git a/package.json b/package.json
index 66a59ed..4348e8e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "theseus-navigator",
- "version": "0.2.2",
+ "version": "0.2.3",
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
"author": "Silent Mode",
"main": "main.js",
@@ -97,9 +97,14 @@
{
"from": "bundled-addons",
"to": "bundled-addons"
+ },
+ {
+ "from": "build/icon.ico",
+ "to": "icon.ico"
}
],
"win": {
+ "icon": "build/icon.ico",
"target": [
"nsis",
"portable"
@@ -110,7 +115,10 @@
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"artifactName": "TheseusNavigator-Setup-${version}.${ext}",
- "include": "nsis/installer.nsh"
+ "include": "nsis/installer.nsh",
+ "installerIcon": "build/icon.ico",
+ "uninstallerIcon": "build/icon.ico",
+ "installerHeaderIcon": "build/icon.ico"
},
"portable": {
"artifactName": "TheseusNavigator-${version}-portable.${ext}"
diff --git a/sidebar-preload.js b/sidebar-preload.js
index ede86cc..218b19e 100644
--- a/sidebar-preload.js
+++ b/sidebar-preload.js
@@ -15,12 +15,60 @@ contextBridge.exposeInMainWorld("silentmode", {
onVisibility: (cb) => ipcRenderer.on("sidebar-visibility", (_e, visible) => cb(!!visible)),
});
+// -- Panel picker strip -----------------------------------------------------
+// Injected at the top of every panel document so the user can switch between
+// registered extension panels without a settings round-trip. Only appears
+// when 2+ panels exist. Main pushes the panel list here via IPC on the
+// initial sidebar-state event.
+async function installPickerStrip() {
+ let state;
+ try { state = await ipcRenderer.invoke("sidebar-state"); } catch { return; }
+ const panels = (state && state.panels) || [];
+ if (panels.length < 2) return;
+ const strip = document.createElement("div");
+ strip.setAttribute("aria-label", "Extension panels");
+ strip.style.cssText = [
+ "position:fixed", "top:0", "left:5px", "right:0", "height:32px",
+ "display:flex", "align-items:stretch", "gap:1px",
+ "background:rgba(11,14,20,.85)", "backdrop-filter:blur(8px)",
+ "border-bottom:1px solid rgba(255,255,255,.10)",
+ "z-index:2147483646", "user-select:none",
+ "font:12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif",
+ ].join(";");
+ const active = state.active;
+ for (const p of panels) {
+ const tab = document.createElement("button");
+ tab.type = "button";
+ const isActive = p.panelId === active;
+ tab.style.cssText = [
+ "flex:1", "min-width:0", "padding:0 10px", "border:0",
+ "background:" + (isActive ? "rgba(214,255,61,.10)" : "transparent"),
+ "color:" + (isActive ? "#d6ff3d" : "#8b98a9"),
+ "cursor:pointer",
+ "display:flex", "align-items:center", "gap:6px", "justify-content:center",
+ "overflow:hidden", "text-overflow:ellipsis", "white-space:nowrap",
+ "font:inherit",
+ ].join(";");
+ tab.innerHTML = '' + (p.icon || "🧩") + '' + p.title.replace(/';
+ tab.addEventListener("click", () => ipcRenderer.invoke("sidebar-open", p.panelId));
+ strip.appendChild(tab);
+ }
+ document.body.appendChild(strip);
+ // Push the panel body down so the 32-px strip doesn't overlap the content.
+ // box-sizing:border-box keeps 100%-tall panels from overflowing when we
+ // add the padding.
+ const style = document.createElement("style");
+ style.textContent = "html,body{box-sizing:border-box}body{padding-top:33px!important}";
+ document.head.appendChild(style);
+}
+
// Sidebar resize grip. Injected into every panel automatically so panel
// authors don't have to reinvent it. A thin strip along the LEFT edge
// (the boundary between the tab area and the sidebar) accepts mousedown
// and streams drag deltas to main until mouseup. Main clamps the width
// to [200, 800] and persists it in settings.sidebarWidth.
window.addEventListener("DOMContentLoaded", () => {
+ installPickerStrip();
const grip = document.createElement("div");
grip.setAttribute("aria-label", "Resize sidebar");
grip.style.cssText = [