Bundle of UX + feature work. Split from packaging by intent so the diff
is reviewable; the next Theseus rebuild ships it.
Features
- Download tracker (new): session.on("will-download") → per-item state
{id, filename, url, mime, total, received, state, savePath, startedAt}
with updated/done event handlers. New downloadsPop WebContentsView
loads downloads.html (new file) + downloads-preload.js (new file);
panel positioned under a new #downloads toolbar button between search
and Tor. Full IPC: downloads-get, toggle/close/resize-downloads,
download-open/show/cancel/clear, downloads-clear-all. In-memory only —
cross-session persistence is a future addition. Button badge shows
active count + spin/done/err color.
- Search engines split by kind + tier:
* kind: "search" | "llm" — separate headers in picker + settings
("Search with" / "Ask an AI"). Empty sections hidden.
* tier: "catalog" | "extra" — Settings now has THREE panes behind the
"+ Add search engine" button: curated catalog, wider discoverable
bank filtered by a live search input, custom URL form.
* DEFAULT_ENABLED unchanged (5 major engines).
* Custom user-added engines carry tier="custom" (never in catalog/extra
panes).
- 9 tier="extra" engines added (all non-login ?q=): Marginalia, Stract,
Yep, Presearch, MetaGer, Qwant, Swisscows, Naver, Baidu. Same rot rule
as LLMs: if one starts bouncing to a login gate, drop it.
Bug fixes
- Loadbar collapses to 0px when idle (was reserving a permanent 2px
strip below the address bar). .loadbar {height:0} + .loadbar.on
{height:2px} + 120ms transition.
- Native <select> popup theme sync via :root { color-scheme: dark } +
@media(prefers-color-scheme: light). nativeTheme.themeSource already
drives prefers-color-scheme, so the OS popup color follows the app
theme automatically (fixed light popup on dark app / vice versa).
- .ctl layout flipped to flex-direction: row with flex-wrap so
anti-fingerprint mode + value fields fit side-by-side.
Settings restructure
- General section: Startup group at the top ("Open previous windows and
tabs" toggle), then Appearance below with three visual THEME CARDS
(System / Light / Dark) — small mock-browser previews per theme,
Firefox-style, active card gets a blue ring. System pipes through to
nativeTheme.themeSource = "system".
- Search promoted to a top-level sidebar item between General and
Naming. Search-engine controls moved out of General into Search.
- Search section: enabled list shows only enabled engines, grouped by
kind, drag-reorder within a kind. "+ Add search engine" opens the
catalog/extras/custom-URL panel.
Search engine catalog trims (already flagged in prior work)
- Removed ChatGPT / Claude / You.com (login-gated ?q=).
- Removed SearXNG (federated; every single-instance default rots).
Docs
- TheseusNavigator/PENDING.md and GOTCHAS.md born with this work
(see the HANDOFF.md commit for the convention).
- PENDING.md's own "session: 2026-08-02:theseus-ux-polish" group will be
emptied after this ship lands.
Preview harness
- _preview.html + _settings-preview.html stubs updated with kind + tier
+ downloads seed + tier="extra" samples so the preview reflects reality.
Both files are gitignored — local only.
Coordination
- Parallel session's collision-policy work (chrome.html registry chips,
popover switcher, Naming section, in-tab collision prompt) already
landed in commits 256079d/42b340f/b0d6375/78dddda. This commit adds
cleanly on top.
101 lines
5.9 KiB
HTML
101 lines
5.9 KiB
HTML
<!doctype html>
|
|
<html><head><meta charset="utf-8">
|
|
<style>
|
|
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
|
|
html, body { margin: 0; background: transparent; }
|
|
.menu { background: #1c222c; border: 1px solid #ffffff26; border-radius: 10px; box-shadow: 0 12px 34px #000c; overflow: hidden; color: #e7eaf1; }
|
|
.head { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px 6px; border-bottom: 1px solid #ffffff12; }
|
|
.head .t { font-size: 11px; letter-spacing: .04em; text-transform: uppercase; color: #8b98a9; }
|
|
.head .clr { font-size: 11.5px; color: #8b98a9; background: transparent; border: none; cursor: pointer; padding: 2px 6px; border-radius: 4px; }
|
|
.head .clr:hover { background: #ffffff10; color: #e7eaf1; }
|
|
.head .clr[disabled] { opacity: .35; cursor: default; }
|
|
.list { max-height: 380px; overflow-y: auto; }
|
|
.empty { padding: 22px 14px; text-align: center; color: #7f8aa0; font-size: 12.5px; }
|
|
.row { padding: 10px 14px; border-bottom: 1px solid #ffffff08; font-size: 12.5px; }
|
|
.row:last-child { border-bottom: 0; }
|
|
.row .top { display: flex; align-items: center; gap: 8px; }
|
|
.row .fn { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #e7eaf1; font-weight: 500; }
|
|
.row .st { font-size: 10.5px; color: #7f8aa0; }
|
|
.row .st.err { color: #f6768a; }
|
|
.row .st.ok { color: #4fd1a5; }
|
|
.row .mt { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 4px; color: #7f8aa0; font-size: 11px; }
|
|
.bar { height: 4px; background: #ffffff10; border-radius: 999px; overflow: hidden; margin-top: 6px; }
|
|
.bar > i { display: block; height: 100%; background: linear-gradient(90deg, #4b7bec, #d6ff3d); border-radius: 999px; transition: width .18s linear; }
|
|
.bar.indet > i { width: 35%; animation: slide 1.1s infinite linear; }
|
|
@keyframes slide { 0% { transform: translateX(-110%); } 100% { transform: translateX(400%); } }
|
|
.btns { display: flex; gap: 4px; }
|
|
.btn { background: transparent; border: none; color: #8b98a9; cursor: pointer; padding: 2px 5px; border-radius: 4px; font-size: 12px; }
|
|
.btn:hover { background: #ffffff10; color: #e7eaf1; }
|
|
.btn.rm:hover { color: #f6768a; }
|
|
@media (prefers-color-scheme: light) {
|
|
.menu { background: #ffffff; border-color: rgba(0,0,0,.15); color: #1a1f28; }
|
|
.head { border-bottom-color: rgba(0,0,0,.08); }
|
|
.head .t, .row .st, .row .mt, .empty, .btn { color: #7b8494; }
|
|
.head .clr:hover, .btn:hover { background: rgba(0,0,0,.05); color: #1a1f28; }
|
|
.row .fn { color: #1a1f28; }
|
|
.row { border-bottom-color: rgba(0,0,0,.06); }
|
|
.bar { background: rgba(0,0,0,.08); }
|
|
}
|
|
</style></head>
|
|
<body>
|
|
<div class="menu">
|
|
<div class="head">
|
|
<span class="t">Downloads</span>
|
|
<button class="clr" id="clr" disabled>Clear finished</button>
|
|
</div>
|
|
<div class="list" id="list"></div>
|
|
</div>
|
|
<script>
|
|
const $ = (id) => document.getElementById(id);
|
|
const esc = (s) => String(s || "").replace(/</g, "<").replace(/"/g, """);
|
|
const fmt = (n) => {
|
|
n = Number(n) || 0;
|
|
if (n < 1024) return n + " B";
|
|
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
|
|
if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + " MB";
|
|
return (n / (1024 * 1024 * 1024)).toFixed(2) + " GB";
|
|
};
|
|
function report() { requestAnimationFrame(() => { try { window.dl.resize(document.querySelector(".menu").offsetHeight); } catch (e) {} }); }
|
|
window.dl.onDownloads((items) => {
|
|
const list = $("list");
|
|
const has = items && items.length;
|
|
$("clr").disabled = !has || !items.some((d) => d.state !== "progressing");
|
|
if (!has) {
|
|
list.innerHTML = `<div class="empty">No downloads yet.</div>`;
|
|
report(); return;
|
|
}
|
|
list.innerHTML = items.map((d) => {
|
|
const done = d.state === "completed";
|
|
const err = d.state === "interrupted";
|
|
const cancel = d.state === "cancelled";
|
|
const inflight = d.state === "progressing" || d.state === "paused";
|
|
const pct = d.total > 0 ? Math.min(100, Math.round((d.received / d.total) * 100)) : 0;
|
|
const barStyle = d.total > 0 ? `width:${pct}%` : "width:35%";
|
|
const barClass = inflight && d.total === 0 ? "bar indet" : "bar";
|
|
const statusText = done ? "Done" : err ? "Failed" : cancel ? "Cancelled" : (d.total > 0 ? pct + "%" : "…");
|
|
const statusCls = done ? "st ok" : (err || cancel) ? "st err" : "st";
|
|
const sizeText = d.total > 0
|
|
? `${fmt(d.received)} / ${fmt(d.total)}`
|
|
: (d.received > 0 ? fmt(d.received) : "");
|
|
const actions = inflight
|
|
? `<button class="btn" data-cancel="${d.id}" title="Cancel">✕</button>`
|
|
: (done
|
|
? `<button class="btn" data-open="${d.id}" title="Open">Open</button>
|
|
<button class="btn" data-show="${d.id}" title="Show in folder">Folder</button>
|
|
<button class="btn rm" data-clear="${d.id}" title="Remove from list">✕</button>`
|
|
: `<button class="btn rm" data-clear="${d.id}" title="Remove from list">✕</button>`);
|
|
return `<div class="row">
|
|
<div class="top"><span class="fn" title="${esc(d.filename)}">${esc(d.filename)}</span><span class="${statusCls}">${statusText}</span></div>
|
|
${inflight || d.total > 0 ? `<div class="${barClass}"><i style="${barStyle}"></i></div>` : ""}
|
|
<div class="mt"><span>${esc(sizeText)}</span><span class="btns">${actions}</span></div>
|
|
</div>`;
|
|
}).join("");
|
|
for (const el of list.querySelectorAll("[data-open]")) el.onclick = () => window.dl.open(+el.dataset.open);
|
|
for (const el of list.querySelectorAll("[data-show]")) el.onclick = () => window.dl.show(+el.dataset.show);
|
|
for (const el of list.querySelectorAll("[data-cancel]"))el.onclick = () => window.dl.cancel(+el.dataset.cancel);
|
|
for (const el of list.querySelectorAll("[data-clear]")) el.onclick = () => window.dl.clear(+el.dataset.clear);
|
|
report();
|
|
});
|
|
$("clr").onclick = () => window.dl.clearAll();
|
|
</script>
|
|
</body></html>
|