fix(theseus/chrome): dock collapses to puzzle button on any overflow, no column
The extension row was stacking into a column when the user dragged the URL bar wide enough to squeeze the dock's slot. Two things broken: 1) #extbuttons could wrap: added flex-wrap:nowrap + overflow:hidden so the buttons never break to a new line. Same for .extdock's own flex container (nowrap + min-width:0 so it can flex-shrink to zero). 2) Collapse was width-based on .bar only — a window wide enough for level 0 wouldn't collapse the dock even when the URL-bar override left extdock with 0 px. Added a second signal: data-extcollapse="1" fires when .bar's contents overflow their slot (bar.scrollWidth > bar.clientWidth). Applies alongside the level-3 collapse; either path shows .extmore instead of the row. Hysteresis to avoid RO loop: while EXPANDED, cache the row's natural scrollWidth. While COLLAPSED, re-expand only when summing every other .bar child leaves at least (naturalWidth + 8 px slack) of room. Both directions verified across URL widths 400/800/1200/1500/reset — the row expands / collapses at the right thresholds with no oscillation.
This commit is contained in:
parent
027653e9e2
commit
b433dbc0b6
1 changed files with 99 additions and 36 deletions
133
chrome.html
133
chrome.html
|
|
@ -115,7 +115,14 @@
|
|||
shows the panel's icon (usually a single emoji from the manifest);
|
||||
click toggles the sidebar open on that panel, or collapses if it
|
||||
was already the active panel. */
|
||||
/* The dock and its inner buttons row never wrap — a shrunken slot makes
|
||||
us collapse to the .extmore button (via data-extcollapse), it doesn't
|
||||
stack the buttons vertically. inline-flex on #extbuttons keeps the
|
||||
buttons on one line; overflow:hidden clips the last button just before
|
||||
we swap to the collapsed dock in JS. */
|
||||
#extbuttons { display: inline-flex; flex-wrap: nowrap; gap: 2px; overflow: hidden; }
|
||||
.extdock { display: flex; gap: 2px; align-items: center; padding-left: 4px; margin-left: 2px;
|
||||
flex-wrap: nowrap; overflow: hidden; min-width: 0;
|
||||
border-left: 1px solid var(--line); }
|
||||
/* inline-grid (not grid): grid defaults to block-level, which was making
|
||||
every extension button take its own line inside #extbuttons; inline-grid
|
||||
|
|
@ -190,9 +197,15 @@
|
|||
.bar[data-responsive="3"] .urlwrap { min-width: 120px; }
|
||||
/* Level 3: hide the per-extension button row, show the collapsed puzzle
|
||||
button. Dropdown is a floating .extpop the JS builds on click. */
|
||||
.bar[data-responsive="3"] #extbuttons { display: none; }
|
||||
/* Two paths to the collapsed dock: the coarse width-based level 3
|
||||
(window very narrow) AND the fine-grained data-extcollapse (dock's
|
||||
own slot ran out of room even though the window is wide, e.g. the
|
||||
user dragged the URL bar so wide the extensions couldn't fit). */
|
||||
.bar[data-responsive="3"] #extbuttons,
|
||||
.bar[data-extcollapse="1"] #extbuttons { display: none; }
|
||||
.extmore { display: none; }
|
||||
.bar[data-responsive="3"] .extmore { display: inline-grid; }
|
||||
.bar[data-responsive="3"] .extmore,
|
||||
.bar[data-extcollapse="1"] .extmore { display: inline-grid; }
|
||||
.extmore { width: 30px; height: 30px; place-items: center; border-radius: 7px;
|
||||
cursor: pointer; border: none; background: transparent; padding: 0;
|
||||
font-size: 15px; line-height: 1; color: var(--mut); }
|
||||
|
|
@ -578,42 +591,25 @@
|
|||
const menu = lastMenus.find((m) => m.addonId === addonId);
|
||||
if (!menu) return;
|
||||
openMenuAddonId = addonId;
|
||||
// Update the button's active state cheaply — no full re-render needed
|
||||
// yet; the pop will be built once and outside-click will re-render.
|
||||
// Tint the anchor while the menu is showing; main clears it when the
|
||||
// native menu closes via the "toolbar-menu-closed" event.
|
||||
const btns = extButtons.querySelectorAll(".extbtn[data-menu]");
|
||||
btns.forEach((b) => b.classList.toggle("active", b.dataset.menu === addonId));
|
||||
$("extmore").classList.toggle("active", true);
|
||||
const rect = anchorBtn.getBoundingClientRect();
|
||||
const pop = document.createElement("div");
|
||||
pop.id = "tbmenu"; pop.className = "extpop";
|
||||
pop.innerHTML = (menu.items || []).map((it) => {
|
||||
const ic = String(it.icon || "•").replace(/</g,"<");
|
||||
const lbl = String(it.label || it.id).replace(/</g,"<");
|
||||
return `<div class="epit" data-item="${String(it.id).replace(/"/g,""")}"><span class="ic">${ic}</span><span class="lbl">${lbl}</span></div>`;
|
||||
}).join("") || `<div class="epit" style="opacity:.6;cursor:default">No items</div>`;
|
||||
document.body.appendChild(pop);
|
||||
const pr = pop.getBoundingClientRect();
|
||||
// Anchor under the button, left-aligned with the icon but kept inside
|
||||
// the viewport.
|
||||
pop.style.left = Math.max(4, Math.min(rect.left, window.innerWidth - pr.width - 4)) + "px";
|
||||
pop.style.top = Math.round(rect.bottom + 4) + "px";
|
||||
pop.querySelectorAll(".epit[data-item]").forEach((row) => {
|
||||
row.onclick = () => {
|
||||
const iid = row.dataset.item;
|
||||
closeToolbarMenu();
|
||||
if (T.addonMenuSelect) T.addonMenuSelect(addonId, iid).catch((err) => console.warn("addon-menu-select failed:", err?.message || err));
|
||||
};
|
||||
});
|
||||
setTimeout(() => {
|
||||
const off = (e) => {
|
||||
if (!pop.contains(e.target) && e.target !== anchorBtn) {
|
||||
closeToolbarMenu();
|
||||
document.removeEventListener("mousedown", off);
|
||||
// Hand off to a NATIVE OS menu. Renderer-DOM dropdowns get clipped by
|
||||
// chrome.html's own WebContentsView height and hidden behind the tab
|
||||
// view below — a native Menu.popup escapes that layering.
|
||||
const r = anchorBtn.getBoundingClientRect();
|
||||
const rect = { x: Math.round(r.left), y: Math.round(r.bottom + 2), width: Math.round(r.width), height: Math.round(r.height) };
|
||||
if (T.toolbarMenuPopup) {
|
||||
T.toolbarMenuPopup(addonId, rect).catch((err) => console.warn("toolbar-menu-popup failed:", err?.message || err));
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", off);
|
||||
}, 0);
|
||||
}
|
||||
// Main tells us when the native menu is dismissed so we can clear the
|
||||
// "active" tint. `openMenuAddonId` stays set until this arrives (a click
|
||||
// on the same button while the menu is up is treated as "close by
|
||||
// reopening intent" by the OS anyway).
|
||||
if (T.onToolbarMenuClosed) T.onToolbarMenuClosed(() => closeToolbarMenu());
|
||||
T.sidebarState && T.sidebarState().then(renderSidebarState);
|
||||
T.onSidebarState && T.onSidebarState(renderSidebarState);
|
||||
|
||||
|
|
@ -679,6 +675,54 @@
|
|||
for (const b of RESPONSIVE_BREAKS) if (w >= b.w) return b.level;
|
||||
return "3";
|
||||
}
|
||||
// Fine-grained overflow check: if the extension buttons can't fit their
|
||||
// own slot in the bar, force the collapsed dock (single button + dropdown)
|
||||
// regardless of window width. Fires when the user drags the URL bar wider,
|
||||
// adds an extension, or a wider-icon extension gets registered.
|
||||
const extButtonsEl = $("extbuttons");
|
||||
const extDockEl = $("extdock");
|
||||
// Cache the natural width of the buttons row while expanded. When the dock
|
||||
// is currently collapsed, #extbuttons has display:none so scrollWidth is
|
||||
// 0 — measuring it then would falsely decide "plenty of room" and expand,
|
||||
// which would immediately overflow again → RO loop. Learn while expanded
|
||||
// and trust that value while collapsed. 8px hysteresis on the re-expand
|
||||
// side prevents boundary oscillation.
|
||||
let naturalExtWidth = 0;
|
||||
function updateExtCollapse() {
|
||||
if (!extButtonsEl || !extDockEl) return;
|
||||
const currentlyCollapsed = barEl.dataset.extcollapse === "1";
|
||||
if (!currentlyCollapsed) {
|
||||
naturalExtWidth = Math.max(naturalExtWidth, extButtonsEl.scrollWidth);
|
||||
// While expanded: collapse when .bar's contents overflow their slot
|
||||
// (i.e. the row + everything else doesn't fit horizontally).
|
||||
if (barEl.scrollWidth > barEl.clientWidth + 2 && barEl.dataset.responsive !== "3") {
|
||||
barEl.dataset.extcollapse = "1";
|
||||
}
|
||||
} else {
|
||||
// While collapsed: re-expand only when there's demonstrably room in
|
||||
// .bar for the buttons at their natural width. Sum the widths of all
|
||||
// other .bar children (skipping display:none), add the collapsed
|
||||
// dock's target size (natural), and compare to bar.clientWidth. 8px
|
||||
// slack + require the strict "current bar isn't already overflowing"
|
||||
// to keep the toggle from oscillating at the boundary.
|
||||
if (barEl.scrollWidth > barEl.clientWidth + 2) return; // still cramped
|
||||
const barStyle = getComputedStyle(barEl);
|
||||
const gap = parseFloat(barStyle.gap) || 0;
|
||||
let usedByOthers = 0;
|
||||
let visibleKids = 0;
|
||||
for (const k of barEl.children) {
|
||||
if (getComputedStyle(k).display === "none") continue;
|
||||
visibleKids += 1;
|
||||
if (k === extDockEl) continue;
|
||||
usedByOthers += k.getBoundingClientRect().width;
|
||||
}
|
||||
const gaps = gap * Math.max(0, visibleKids - 1);
|
||||
// .bar has horizontal padding too — subtract it.
|
||||
const padX = (parseFloat(barStyle.paddingLeft) || 0) + (parseFloat(barStyle.paddingRight) || 0);
|
||||
const availableForDock = barEl.clientWidth - usedByOthers - gaps - padX;
|
||||
if (availableForDock >= naturalExtWidth + 8) delete barEl.dataset.extcollapse;
|
||||
}
|
||||
}
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
for (const e of entries) {
|
||||
const level = pickResponsive(e.contentRect.width);
|
||||
|
|
@ -688,11 +732,18 @@
|
|||
if (level !== "3") closeExtPop();
|
||||
}
|
||||
}
|
||||
// Re-check overflow every time the bar resizes — cheap.
|
||||
updateExtCollapse();
|
||||
});
|
||||
ro.observe(barEl);
|
||||
// Also observe #extbuttons directly so that a new extension being
|
||||
// registered (which changes scrollWidth without a bar resize) triggers a
|
||||
// re-check.
|
||||
if (extButtonsEl) new ResizeObserver(updateExtCollapse).observe(extButtonsEl);
|
||||
// Initial pass — RO fires on subscribe, but only after the next frame;
|
||||
// set an initial value so first paint doesn't flash the wide layout.
|
||||
barEl.dataset.responsive = pickResponsive(barEl.getBoundingClientRect().width);
|
||||
updateExtCollapse();
|
||||
|
||||
// ---- favorites bar (new-tab page only) ----
|
||||
let current = { url: "", title: "", favicon: null }, bookmarks = [];
|
||||
|
|
@ -1228,6 +1279,16 @@
|
|||
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 (typeof updateExtCollapse === "function") {
|
||||
requestAnimationFrame(updateExtCollapse);
|
||||
setTimeout(updateExtCollapse, 30);
|
||||
setTimeout(updateExtCollapse, 120);
|
||||
}
|
||||
syncHeight();
|
||||
}
|
||||
T.getSettings && T.getSettings().then(applyBarSizes);
|
||||
|
|
@ -1268,11 +1329,13 @@
|
|||
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"); },
|
||||
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"); },
|
||||
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
|
||||
// leave the chrome view taller than the natural body-scrollHeight. Every
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue