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
it via Settings > Appearance > Address bar size so the extension dock
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; }
/* Capped URL bar: pin the trailing items (download / extensions / logo /
Theseus) to the RIGHT edge instead of packing them next to the URL bar
— a smaller address bar shouldn't slide everything leftward. margin-
right: auto on the URL bar consumes the slack. */
.bar[data-urlsize="medium"] .urlwrap { flex: 0 1 640px; margin-right: auto; }
.bar[data-urlsize="compact"] .urlwrap { flex: 0 1 400px; margin-right: auto; }
.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); }
.searchbox { flex: 1 1 0; min-width: 140px; }
/* Single drag handle sitting between the two — a thin 6px column with a
col-resize cursor. Zero-sum semantics: dragging right grows the URL
bar and shrinks the search bar; dragging left does the inverse. */
.bardrag { flex: 0 0 6px; align-self: stretch; cursor: col-resize; background: transparent; border-radius: 3px; }
.bardrag:hover, .bardrag.dragging { background: rgb(from var(--acid) r g b / .35); }
/* Adaptive toolbar. A ResizeObserver on .bar sets data-responsive:
"0" wide — everything visible (default)
"1" tight — auto-hide the search box, min-width for URL bar drops
@ -192,14 +186,17 @@
narrow widths yields to the responsive collapse. */
.bar[data-responsive="1"] .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="3"] .logo span:not(.gear) { display: none; }
.bar[data-responsive="2"] .logo,
.bar[data-responsive="3"] .logo { padding: 4px 8px; }
.bar[data-responsive="1"] .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
button. Dropdown is a floating .extpop the JS builds on click. */
/* 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:hover { background: rgba(63,185,80,.14); }
.pwchip svg { width: 14px; height: 14px; }
/* 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);
/* search box: engine icon (click = pick engine) + a wide typing area.
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; }
.searchbox:focus-within { border-color: #4b7bec; }
/* 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="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 class="urlsearch" id="urlsearch">
<div class="urlwrap">
<button class="secbadge" id="secbadge" title="Site information">
<!-- Heraldic shield outline matching the reference image: flared top
@ -404,13 +404,13 @@
<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="star" id="star" title="Save this page"></button>
<div class="urldrag" id="urldrag" title="Drag to resize"></div>
</div>
<div class="bardrag" id="bardrag" title="Drag to shift the address / search bar ratio"></div>
<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>
<input id="search" placeholder="Search" spellcheck="false">
</div>
</div>
<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>
<span class="dlbadge" id="dlbadge" hidden>0</span>
@ -1352,27 +1352,22 @@
});
syncHeight();
// Toolbar sizing: URL bar + search box widths follow user preference.
// Applied as data-attrs on .bar so CSS handles the layout swap.
// urlBarWidthPx / searchBoxWidthPx (integers > 0) come from the drag
// handles; when present they OVERRIDE the discrete size preset. Set to
// null (or 0) to restore preset behavior.
// Toolbar sizing: only urlBarWidthPx now matters. The URL bar and the
// search bar share one flex budget in .urlsearch — bigger URL bar means
// smaller search bar and vice versa, without moving any of the fixed
// items on either side. Old urlBarSize / searchBoxSize / searchBoxWidthPx
// 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) {
if (!s) return;
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;
if (uw > 0) { bar.dataset.urlwidth = "1"; bar.style.setProperty("--urlbar-w", uw + "px"); }
else { delete bar.dataset.urlwidth; bar.style.removeProperty("--urlbar-w"); }
const sw = Number(s.searchBoxWidthPx) || 0;
if (sw > 0) { bar.dataset.searchwidth = "1"; bar.style.setProperty("--searchbox-w", sw + "px"); }
else { delete bar.dataset.searchwidth; bar.style.removeProperty("--searchbox-w"); }
// The URL/search width change squeezes the extension dock; the RO on
// #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 (uw > 0) bar.style.setProperty("--urlbar-w", uw + "px");
else bar.style.removeProperty("--urlbar-w");
// The URL/search ratio change doesn't squeeze the extension dock any
// more (they live in different flex parents), but the responsive-hide
// rules for narrow bars still depend on updateExtCollapse's math, so
// re-check overflow after the layout settles.
if (typeof updateExtCollapse === "function") {
requestAnimationFrame(updateExtCollapse);
setTimeout(updateExtCollapse, 30);
@ -1437,49 +1432,48 @@
$("findNext").onclick = () => stepFind(true);
$("findClose").onclick = closeFind;
// Drag handles: right edge of .urlwrap resizes the URL bar; left edge of
// .searchbox resizes the search box. Persists to settings.urlBarWidthPx /
// searchBoxWidthPx on release, which then wins over the size preset.
function makeDragHandle(handleEl, targetEl, opts) {
let startX = 0, startW = 0, dragging = false, lastW = 0;
// Single drag handle between the address bar and the search bar (Firefox
// model): dragging right grows the URL bar and shrinks the search bar,
// dragging left inverts. Zero-sum — nothing else in the toolbar moves.
// The URL bar's width is what we control directly; the search bar just
// 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) => {
if (e.button !== 0) return;
dragging = true;
startX = e.clientX;
startW = targetEl.getBoundingClientRect().width;
startW = urlWrapEl.getBoundingClientRect().width;
handleEl.setPointerCapture(e.pointerId);
handleEl.classList.add("dragging");
e.preventDefault();
});
handleEl.addEventListener("pointermove", (e) => {
if (!dragging) return;
const dx = e.clientX - startX;
// Left-edge handles (search) grow when dragged LEFT (dx < 0).
const w = Math.round(Math.max(opts.min, Math.min(opts.max, startW + (opts.reverseX ? -dx : dx))));
const total = urlsearchEl.getBoundingClientRect().width;
const maxUrl = Math.max(MIN_URL, total - HANDLE_W - GAP * 2 - MIN_SEARCH);
const w = Math.round(Math.max(MIN_URL, Math.min(maxUrl, startW + (e.clientX - startX))));
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;
dragging = false;
handleEl.classList.remove("dragging");
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("pointercancel", stop);
}
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(); },
});
handleEl.addEventListener("pointerup", stopDrag);
handleEl.addEventListener("pointercancel", stopDrag);
// Self-heal: some menu-close paths (drag interruptions, focus flips) can
// 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

View file

@ -1,6 +1,6 @@
{
"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.",
"author": "Silent Mode",
"main": "main.js",