feat(theseus/toolbar): Firefox-style single drag handle, zero-sum URL/search ratio

The two independent drag handles (right edge of URL bar, left edge of
search bar) are replaced with ONE handle sitting between them, wrapped
in a new .urlsearch flex container that owns the URL-bar-plus-search-bar
budget between the nav buttons on the left and the trailing dock on the
right.

Zero-sum semantics: dragging the handle right grows the URL bar and
shrinks the search bar; dragging left inverts. Nothing else in the
toolbar moves — nav buttons, Downloads, extension dock and the
⛓ Theseus logo all sit outside .urlsearch's flex parent.

When the window shrinks below the responsive breakpoint the search
bar (and the drag handle) hide as before, and the URL bar grows to
fill the .urlsearch budget on its own (added flex: 1 1 auto in the
data-responsive rules for .urlwrap).

Verified static rendering: url=700px → search=157px, url=300px →
search=539px, url=default → 60/40 split, narrow window → search
hidden and url fills.

Old settings (urlBarSize / searchBoxSize / searchBoxWidthPx) are
ignored by chrome.html but kept in main's SETTINGS_DEFAULTS so an
older chrome.html could still read them on a downgrade. Only
urlBarWidthPx is used going forward.
This commit is contained in:
Local Dev 2026-09-09 00:11:26 +02:00
parent 71bd3004e0
commit e36d2b361c
2 changed files with 96 additions and 102 deletions

View file

@ -159,28 +159,22 @@
/* address bar. flex:1 by default consumes remaining space; user can cap /* address bar. flex:1 by default consumes remaining space; user can cap
it via Settings > Appearance > Address bar size so the extension dock it via Settings > Appearance > Address bar size so the extension dock
gets more room. min-width guards against squeezing the URL invisible. */ gets more room. min-width guards against squeezing the URL invisible. */
.urlwrap { position: relative; flex: 1 1 auto; min-width: 200px; display: flex; align-items: center; gap: 4px; background: var(--surface); border: 1px solid var(--line); /* The address bar and search bar share ONE flex budget between the nav
buttons and the trailing button dock — the whole .urlsearch unit takes
whatever space is left after the fixed items. A single drag handle
between them shifts the ratio (bigger URL bar = smaller search bar,
and vice versa) without moving anything else in the toolbar. When the
window is too narrow for both, the search bar hides (see the
data-responsive rules below); the URL bar keeps working alone. */
.urlsearch { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 6px; }
.urlwrap { position: relative; flex: 0 1 var(--urlbar-w, 60%); min-width: 220px; display: flex; align-items: center; gap: 4px; background: var(--surface); border: 1px solid var(--line);
border-radius: 999px; padding: 0 6px 0 4px; } border-radius: 999px; padding: 0 6px 0 4px; }
/* Capped URL bar: pin the trailing items (download / extensions / logo / .searchbox { flex: 1 1 0; min-width: 140px; }
Theseus) to the RIGHT edge instead of packing them next to the URL bar /* Single drag handle sitting between the two — a thin 6px column with a
— a smaller address bar shouldn't slide everything leftward. margin- col-resize cursor. Zero-sum semantics: dragging right grows the URL
right: auto on the URL bar consumes the slack. */ bar and shrinks the search bar; dragging left does the inverse. */
.bar[data-urlsize="medium"] .urlwrap { flex: 0 1 640px; margin-right: auto; } .bardrag { flex: 0 0 6px; align-self: stretch; cursor: col-resize; background: transparent; border-radius: 3px; }
.bar[data-urlsize="compact"] .urlwrap { flex: 0 1 400px; margin-right: auto; } .bardrag:hover, .bardrag.dragging { background: rgb(from var(--acid) r g b / .35); }
.bar[data-urlwidth] .urlwrap { flex: 0 0 var(--urlbar-w, auto); margin-right: auto; }
.searchbox { position: relative; }
.bar[data-searchsize="hidden"] .searchbox { display: none; }
.bar[data-searchsize="compact"] .searchbox { width: 180px; }
.bar[data-searchsize="wide"] .searchbox { width: 400px; }
.bar[data-searchwidth] .searchbox { width: var(--searchbox-w, 300px); }
/* Drag-to-resize handles: a thin invisible strip on the trailing edge of
.urlwrap and the leading edge of .searchbox. col-resize cursor makes
the affordance visible; a faint acid tint on hover confirms it's live. */
.urldrag, .searchdrag { position: absolute; top: 0; bottom: 0; width: 6px; cursor: col-resize; z-index: 10; background: transparent; }
.urldrag { right: -3px; }
.searchdrag { left: -3px; }
.urldrag:hover, .searchdrag:hover,
.urldrag.dragging, .searchdrag.dragging { background: rgb(from var(--acid) r g b / .35); }
/* Adaptive toolbar. A ResizeObserver on .bar sets data-responsive: /* Adaptive toolbar. A ResizeObserver on .bar sets data-responsive:
"0" wide — everything visible (default) "0" wide — everything visible (default)
"1" tight — auto-hide the search box, min-width for URL bar drops "1" tight — auto-hide the search box, min-width for URL bar drops
@ -192,14 +186,17 @@
narrow widths yields to the responsive collapse. */ narrow widths yields to the responsive collapse. */
.bar[data-responsive="1"] .searchbox, .bar[data-responsive="1"] .searchbox,
.bar[data-responsive="2"] .searchbox, .bar[data-responsive="2"] .searchbox,
.bar[data-responsive="3"] .searchbox { display: none; } .bar[data-responsive="3"] .searchbox,
.bar[data-responsive="1"] .bardrag,
.bar[data-responsive="2"] .bardrag,
.bar[data-responsive="3"] .bardrag { display: none; }
.bar[data-responsive="2"] .logo span:not(.gear), .bar[data-responsive="2"] .logo span:not(.gear),
.bar[data-responsive="3"] .logo span:not(.gear) { display: none; } .bar[data-responsive="3"] .logo span:not(.gear) { display: none; }
.bar[data-responsive="2"] .logo, .bar[data-responsive="2"] .logo,
.bar[data-responsive="3"] .logo { padding: 4px 8px; } .bar[data-responsive="3"] .logo { padding: 4px 8px; }
.bar[data-responsive="1"] .urlwrap, .bar[data-responsive="1"] .urlwrap,
.bar[data-responsive="2"] .urlwrap, .bar[data-responsive="2"] .urlwrap,
.bar[data-responsive="3"] .urlwrap { min-width: 120px; } .bar[data-responsive="3"] .urlwrap { min-width: 120px; flex: 1 1 auto; }
/* Level 3: hide the per-extension button row, show the collapsed puzzle /* Level 3: hide the per-extension button row, show the collapsed puzzle
button. Dropdown is a floating .extpop the JS builds on click. */ button. Dropdown is a floating .extpop the JS builds on click. */
/* Two paths to the collapsed dock: the coarse width-based level 3 /* Two paths to the collapsed dock: the coarse width-based level 3
@ -279,8 +276,10 @@
.pwchip { display: inline-flex; align-items: center; gap: 3px; border: none; background: transparent; color: #3fb950; cursor: pointer; padding: 2px 5px; border-radius: 6px; font-size: 11px; font-weight: 700; } .pwchip { display: inline-flex; align-items: center; gap: 3px; border: none; background: transparent; color: #3fb950; cursor: pointer; padding: 2px 5px; border-radius: 6px; font-size: 11px; font-weight: 700; }
.pwchip:hover { background: rgba(63,185,80,.14); } .pwchip:hover { background: rgba(63,185,80,.14); }
.pwchip svg { width: 14px; height: 14px; } .pwchip svg { width: 14px; height: 14px; }
/* search box: engine icon (click = pick engine) + a wide typing area */ /* search box: engine icon (click = pick engine) + a wide typing area.
.searchbox { display: flex; align-items: center; width: 300px; background: var(--surface); border: 1px solid var(--line); Width comes from the parent .urlsearch flex budget (flex: 1 1 0) —
the earlier top rule at line 172 sets that; here we only style. */
.searchbox { display: flex; align-items: center; background: var(--surface); border: 1px solid var(--line);
border-radius: 999px; padding: 0 12px 0 3px; } border-radius: 999px; padding: 0 12px 0 3px; }
.searchbox:focus-within { border-color: #4b7bec; } .searchbox:focus-within { border-color: #4b7bec; }
/* dropdown button shows the SELECTED engine's favicon (emoji fallback) + a caret */ /* dropdown button shows the SELECTED engine's favicon (emoji fallback) + a caret */
@ -391,6 +390,7 @@
<button class="ic" id="reload" title="Reload"><svg viewBox="0 0 16 16"><path d="M13 8 a5 5 0 1 1 -1.5 -3.6"/><path d="M13 2.5 L13 5 L10.5 5"/></svg></button> <button class="ic" id="reload" title="Reload"><svg viewBox="0 0 16 16"><path d="M13 8 a5 5 0 1 1 -1.5 -3.6"/><path d="M13 2.5 L13 5 L10.5 5"/></svg></button>
<button class="ic" id="home" title="Home"><svg viewBox="0 0 16 16"><path d="M2.5 7.5 L8 3 L13.5 7.5"/><path d="M4 6.7 L4 13 L12 13 L12 6.7"/></svg></button> <button class="ic" id="home" title="Home"><svg viewBox="0 0 16 16"><path d="M2.5 7.5 L8 3 L13.5 7.5"/><path d="M4 6.7 L4 13 L12 13 L12 6.7"/></svg></button>
</div> </div>
<div class="urlsearch" id="urlsearch">
<div class="urlwrap"> <div class="urlwrap">
<button class="secbadge" id="secbadge" title="Site information"> <button class="secbadge" id="secbadge" title="Site information">
<!-- Heraldic shield outline matching the reference image: flared top <!-- Heraldic shield outline matching the reference image: flared top
@ -404,13 +404,13 @@
<span class="reg" id="reg" hidden></span> <span class="reg" id="reg" hidden></span>
<button class="pwchip" id="pwchip" title="Fill password for this site" hidden><span id="pwchipCount"></span><svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="6" cy="9" r="3.2" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M8.8 8.6 L14 8.6 M13.2 8.6 L13.2 11 M11.5 8.6 L11.5 10.4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button> <button class="pwchip" id="pwchip" title="Fill password for this site" hidden><span id="pwchipCount"></span><svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="6" cy="9" r="3.2" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M8.8 8.6 L14 8.6 M13.2 8.6 L13.2 11 M11.5 8.6 L11.5 10.4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>
<button class="star" id="star" title="Save this page"></button> <button class="star" id="star" title="Save this page"></button>
<div class="urldrag" id="urldrag" title="Drag to resize"></div>
</div> </div>
<div class="bardrag" id="bardrag" title="Drag to shift the address / search bar ratio"></div>
<div class="searchbox"> <div class="searchbox">
<div class="searchdrag" id="searchdrag" title="Drag to resize"></div>
<button id="engineBtn" class="engine-btn" title="Choose search engine"><span id="engineIcon"></span><span class="cv"></span></button> <button id="engineBtn" class="engine-btn" title="Choose search engine"><span id="engineIcon"></span><span class="cv"></span></button>
<input id="search" placeholder="Search" spellcheck="false"> <input id="search" placeholder="Search" spellcheck="false">
</div> </div>
</div>
<button class="ic dlbtn" id="downloads" title="Downloads"> <button class="ic dlbtn" id="downloads" title="Downloads">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2 V10 M4.5 6.5 L8 10 L11.5 6.5"/><path d="M3 12.5 L3 13.5 L13 13.5 L13 12.5"/></svg> <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2 V10 M4.5 6.5 L8 10 L11.5 6.5"/><path d="M3 12.5 L3 13.5 L13 13.5 L13 12.5"/></svg>
<span class="dlbadge" id="dlbadge" hidden>0</span> <span class="dlbadge" id="dlbadge" hidden>0</span>
@ -1352,27 +1352,22 @@
}); });
syncHeight(); syncHeight();
// Toolbar sizing: URL bar + search box widths follow user preference. // Toolbar sizing: only urlBarWidthPx now matters. The URL bar and the
// Applied as data-attrs on .bar so CSS handles the layout swap. // search bar share one flex budget in .urlsearch — bigger URL bar means
// urlBarWidthPx / searchBoxWidthPx (integers > 0) come from the drag // smaller search bar and vice versa, without moving any of the fixed
// handles; when present they OVERRIDE the discrete size preset. Set to // items on either side. Old urlBarSize / searchBoxSize / searchBoxWidthPx
// null (or 0) to restore preset behavior. // settings are ignored (kept in the settings file for backward compat
// so an older version's chrome.html can still pick them up).
function applyBarSizes(s) { function applyBarSizes(s) {
if (!s) return; if (!s) return;
const bar = $("bar"); const bar = $("bar");
if (s.urlBarSize) bar.dataset.urlsize = s.urlBarSize;
if (s.searchBoxSize) bar.dataset.searchsize = s.searchBoxSize;
const uw = Number(s.urlBarWidthPx) || 0; const uw = Number(s.urlBarWidthPx) || 0;
if (uw > 0) { bar.dataset.urlwidth = "1"; bar.style.setProperty("--urlbar-w", uw + "px"); } if (uw > 0) bar.style.setProperty("--urlbar-w", uw + "px");
else { delete bar.dataset.urlwidth; bar.style.removeProperty("--urlbar-w"); } else bar.style.removeProperty("--urlbar-w");
const sw = Number(s.searchBoxWidthPx) || 0; // The URL/search ratio change doesn't squeeze the extension dock any
if (sw > 0) { bar.dataset.searchwidth = "1"; bar.style.setProperty("--searchbox-w", sw + "px"); } // more (they live in different flex parents), but the responsive-hide
else { delete bar.dataset.searchwidth; bar.style.removeProperty("--searchbox-w"); } // rules for narrow bars still depend on updateExtCollapse's math, so
// The URL/search width change squeezes the extension dock; the RO on // re-check overflow after the layout settles.
// #extbuttons doesn't always fire (overflow:hidden parent), so re-check
// overflow explicitly after the next layout pass. requestAnimationFrame
// isn't always enough (the CSS var propagation + reflow can straddle
// frames), hence the belt-and-braces setTimeout too.
if (typeof updateExtCollapse === "function") { if (typeof updateExtCollapse === "function") {
requestAnimationFrame(updateExtCollapse); requestAnimationFrame(updateExtCollapse);
setTimeout(updateExtCollapse, 30); setTimeout(updateExtCollapse, 30);
@ -1437,49 +1432,48 @@
$("findNext").onclick = () => stepFind(true); $("findNext").onclick = () => stepFind(true);
$("findClose").onclick = closeFind; $("findClose").onclick = closeFind;
// Drag handles: right edge of .urlwrap resizes the URL bar; left edge of // Single drag handle between the address bar and the search bar (Firefox
// .searchbox resizes the search box. Persists to settings.urlBarWidthPx / // model): dragging right grows the URL bar and shrinks the search bar,
// searchBoxWidthPx on release, which then wins over the size preset. // dragging left inverts. Zero-sum — nothing else in the toolbar moves.
function makeDragHandle(handleEl, targetEl, opts) { // The URL bar's width is what we control directly; the search bar just
let startX = 0, startW = 0, dragging = false, lastW = 0; // fills whatever's left of the .urlsearch flex budget. Persisted as
// urlBarWidthPx on release. Bounds:
// min = URL bar's own min-width (matches the 220px CSS floor)
// max = urlsearch total width handle gap minSearch(140)
// so the drag can never squeeze the search bar past its CSS floor either.
const barEl2 = $("bar");
const handleEl = $("bardrag");
const urlWrapEl = barEl2.querySelector(".urlwrap");
const urlsearchEl = $("urlsearch");
const MIN_URL = 220, MIN_SEARCH = 140, HANDLE_W = 6, GAP = 6;
let dragging = false, startX = 0, startW = 0, lastW = 0;
handleEl.addEventListener("pointerdown", (e) => { handleEl.addEventListener("pointerdown", (e) => {
if (e.button !== 0) return; if (e.button !== 0) return;
dragging = true; dragging = true;
startX = e.clientX; startX = e.clientX;
startW = targetEl.getBoundingClientRect().width; startW = urlWrapEl.getBoundingClientRect().width;
handleEl.setPointerCapture(e.pointerId); handleEl.setPointerCapture(e.pointerId);
handleEl.classList.add("dragging"); handleEl.classList.add("dragging");
e.preventDefault(); e.preventDefault();
}); });
handleEl.addEventListener("pointermove", (e) => { handleEl.addEventListener("pointermove", (e) => {
if (!dragging) return; if (!dragging) return;
const dx = e.clientX - startX; const total = urlsearchEl.getBoundingClientRect().width;
// Left-edge handles (search) grow when dragged LEFT (dx < 0). const maxUrl = Math.max(MIN_URL, total - HANDLE_W - GAP * 2 - MIN_SEARCH);
const w = Math.round(Math.max(opts.min, Math.min(opts.max, startW + (opts.reverseX ? -dx : dx)))); const w = Math.round(Math.max(MIN_URL, Math.min(maxUrl, startW + (e.clientX - startX))));
lastW = w; lastW = w;
opts.applyLive(w); barEl2.style.setProperty("--urlbar-w", w + "px");
if (typeof updateExtCollapse === "function") updateExtCollapse();
}); });
const stop = (e) => { const stopDrag = (e) => {
if (!dragging) return; if (!dragging) return;
dragging = false; dragging = false;
handleEl.classList.remove("dragging"); handleEl.classList.remove("dragging");
try { handleEl.releasePointerCapture(e.pointerId); } catch {} try { handleEl.releasePointerCapture(e.pointerId); } catch {}
if (lastW > 0 && T.setSetting) T.setSetting(opts.settingKey, lastW); if (lastW > 0 && T.setSetting) T.setSetting("urlBarWidthPx", lastW);
}; };
handleEl.addEventListener("pointerup", stop); handleEl.addEventListener("pointerup", stopDrag);
handleEl.addEventListener("pointercancel", stop); handleEl.addEventListener("pointercancel", stopDrag);
}
const barEl2 = $("bar");
makeDragHandle($("urldrag"), $("bar").querySelector(".urlwrap"), {
min: 200, max: 1800, settingKey: "urlBarWidthPx", reverseX: false,
applyLive: (w) => { barEl2.dataset.urlwidth = "1"; barEl2.style.setProperty("--urlbar-w", w + "px");
if (typeof updateExtCollapse === "function") updateExtCollapse(); },
});
makeDragHandle($("searchdrag"), $("bar").querySelector(".searchbox"), {
min: 120, max: 800, settingKey: "searchBoxWidthPx", reverseX: true,
applyLive: (w) => { barEl2.dataset.searchwidth = "1"; barEl2.style.setProperty("--searchbox-w", w + "px");
if (typeof updateExtCollapse === "function") updateExtCollapse(); },
});
// Self-heal: some menu-close paths (drag interruptions, focus flips) can // Self-heal: some menu-close paths (drag interruptions, focus flips) can
// leave the chrome view taller than the natural body-scrollHeight. Every // leave the chrome view taller than the natural body-scrollHeight. Every
// 750 ms with no menu open, re-sync — main.js only re-layouts when the // 750 ms with no menu open, re-sync — main.js only re-layouts when the

View file

@ -1,6 +1,6 @@
{ {
"name": "theseus-navigator", "name": "theseus-navigator",
"version": "0.3.36", "version": "0.3.37",
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
"author": "Silent Mode", "author": "Silent Mode",
"main": "main.js", "main": "main.js",