feat(theseus/toolbar): drag-handles + pin right-side items on URL/search resize

Two fixes bundled:

1) Right-side items (download, extension dock, Theseus button) no longer
   slide leftward when the URL bar is capped. Added margin-right: auto
   to .urlwrap under every capped state (data-urlsize=medium/compact
   and the new data-urlwidth override) so the leftover flex space sits
   AFTER the URL bar, keeping the right group pinned to the right edge.
   Verified: logo's gap from bar's right edge stays at 10px across
   default / urlBarSize=compact / urlBarWidthPx=500 / reset.

2) Drag handles for live resize. A 6-px col-resize strip sits on the
   trailing edge of .urlwrap and the leading edge of .searchbox; a
   pointerdown/move/up dance updates the width live via a CSS custom
   property and persists to settings on release. Two new keys:
   - urlBarWidthPx (0-1800, 0 = follow size preset)
   - searchBoxWidthPx (0-800, 0 = follow size preset)
   When either is non-zero, the corresponding CSS override wins over
   the discrete size preset. Preload gets setSetting so the drag can
   push the persistent value from chrome.

Visual affordance: handles are transparent by default, gain a faint
acid tint on hover and while dragging.
This commit is contained in:
Local Dev 2026-09-07 01:07:23 +02:00
parent c55570e4bf
commit 909b93ee23
3 changed files with 76 additions and 3 deletions

View file

@ -147,13 +147,28 @@
/* 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 { flex: 1 1 auto; min-width: 200px; display: flex; align-items: center; gap: 4px; background: var(--surface); border: 1px solid var(--line); .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);
border-radius: 999px; padding: 0 6px 0 4px; } border-radius: 999px; padding: 0 6px 0 4px; }
.bar[data-urlsize="medium"] .urlwrap { flex: 0 1 640px; } /* Capped URL bar: pin the trailing items (download / extensions / logo /
.bar[data-urlsize="compact"] .urlwrap { flex: 0 1 400px; } 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="hidden"] .searchbox { display: none; }
.bar[data-searchsize="compact"] .searchbox { width: 180px; } .bar[data-searchsize="compact"] .searchbox { width: 180px; }
.bar[data-searchsize="wide"] .searchbox { width: 400px; } .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: rgba(214,255,61,.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
@ -339,8 +354,10 @@
<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="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>
@ -1197,15 +1214,66 @@
syncHeight(); syncHeight();
// Toolbar sizing: URL bar + search box widths follow user preference. // Toolbar sizing: URL bar + search box widths follow user preference.
// Applied as data-attrs on .bar so CSS handles the layout swap. // 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.
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.urlBarSize) bar.dataset.urlsize = s.urlBarSize;
if (s.searchBoxSize) bar.dataset.searchsize = s.searchBoxSize; 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"); }
syncHeight(); syncHeight();
} }
T.getSettings && T.getSettings().then(applyBarSizes); T.getSettings && T.getSettings().then(applyBarSizes);
T.onSettingsUpdate && T.onSettingsUpdate(applyBarSizes); T.onSettingsUpdate && T.onSettingsUpdate(applyBarSizes);
// 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;
handleEl.addEventListener("pointerdown", (e) => {
if (e.button !== 0) return;
dragging = true;
startX = e.clientX;
startW = targetEl.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))));
lastW = w;
opts.applyLive(w);
});
const stop = (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);
};
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"); },
});
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"); },
});
// 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

@ -230,6 +230,10 @@ const SETTINGS_DEFAULTS = {
// room to grow. The search box is fixed-width; hidden removes it. // room to grow. The search box is fixed-width; hidden removes it.
urlBarSize: "wide", // compact | medium | wide (default) urlBarSize: "wide", // compact | medium | wide (default)
searchBoxSize: "normal", // hidden | compact | normal (default) | wide searchBoxSize: "normal", // hidden | compact | normal (default) | wide
// Drag-set widths in pixels. Non-zero → override the corresponding size
// preset; zero/null → follow the preset. Set by the toolbar drag handles.
urlBarWidthPx: 0,
searchBoxWidthPx: 0,
}; };
// Applying the theme via nativeTheme.themeSource makes prefers-color-scheme update // Applying the theme via nativeTheme.themeSource makes prefers-color-scheme update
// in every renderer (chrome, settings, popover, page views) with no per-view IPC. // in every renderer (chrome, settings, popover, page views) with no per-view IPC.

View file

@ -26,6 +26,7 @@ contextBridge.exposeInMainWorld("theseus", {
toggleTor: () => ipcRenderer.invoke("toggle-tor"), toggleTor: () => ipcRenderer.invoke("toggle-tor"),
openSettings: () => ipcRenderer.invoke("open-settings"), openSettings: () => ipcRenderer.invoke("open-settings"),
getSettings: () => ipcRenderer.invoke("settings-get"), getSettings: () => ipcRenderer.invoke("settings-get"),
setSetting: (key, val) => ipcRenderer.invoke("settings-set", key, val),
onSettingsUpdate: (cb) => ipcRenderer.on("settings-update", (_e, d) => cb(d)), onSettingsUpdate: (cb) => ipcRenderer.on("settings-update", (_e, d) => cb(d)),
toggleSiteInfo: (rect) => ipcRenderer.invoke("toggle-site-info", rect), toggleSiteInfo: (rect) => ipcRenderer.invoke("toggle-site-info", rect),
switchToBcnr: () => ipcRenderer.invoke("switch-to-bcnr"), switchToBcnr: () => ipcRenderer.invoke("switch-to-bcnr"),