feat(theseus/chrome): adaptive toolbar — drops search, collapses Theseus, folds extension dock
A ResizeObserver on .bar sets data-responsive to one of four levels
based on width, and CSS reacts:
0 (>=1050px) wide — everything visible (default)
1 ( >=820px) tight — auto-hide the search box, URL min-width drops
2 ( >=620px) narrow — Theseus button collapses to just the gear
3 ( <) xnarrow — extension dock collapses to a single 🧩 button
that opens a dropdown listing every registered
panel (icon + name), click to open the sidebar
Puzzle dropdown reuses the same open/close pattern as the existing
bookmarks menu — click outside to dismiss, click a row to open (or
close if it's already the active panel). User's explicit
Settings > Address bar size / Search box size still win when the
toolbar is wide enough for them; responsive collapse only forces
extra hides at the tighter widths.
Verified on a running instance across 5 widths (1200/900/700/500/400):
each breakpoint flips exactly the elements it should.
This commit is contained in:
parent
354606f3c1
commit
f36e1db17f
1 changed files with 101 additions and 0 deletions
101
chrome.html
101
chrome.html
|
|
@ -154,6 +154,44 @@
|
||||||
.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; }
|
||||||
|
/* 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
|
||||||
|
"2" narrow — Theseus button collapses to just the gear icon
|
||||||
|
"3" xnarrow — extension dock collapses to a single 🧩 puzzle button
|
||||||
|
that opens a dropdown of every registered addon panel
|
||||||
|
User's explicit data-searchsize wins over auto-hide at level 1: if the
|
||||||
|
user picked "wide" or "normal" they still get it, but "normal" at very
|
||||||
|
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="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; }
|
||||||
|
/* 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; }
|
||||||
|
.extmore { display: none; }
|
||||||
|
.bar[data-responsive="3"] .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); }
|
||||||
|
.extmore:hover { background: var(--line2); color: var(--ink); }
|
||||||
|
.extmore.active { background: rgba(214,255,61,.12); color: var(--acid); }
|
||||||
|
.extpop { position: fixed; z-index: 9999; background: var(--surface, #1c222c);
|
||||||
|
border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 12px 34px #000c;
|
||||||
|
padding: 4px; min-width: 180px; }
|
||||||
|
.extpop .epit { display: flex; align-items: center; gap: 10px; padding: 7px 10px;
|
||||||
|
border-radius: 6px; cursor: pointer; font-size: 13px; color: var(--ink); }
|
||||||
|
.extpop .epit:hover { background: var(--hover, #ffffff10); }
|
||||||
|
.extpop .epit.active { background: rgba(214,255,61,.10); color: var(--acid); }
|
||||||
|
.extpop .epit .ic { width: 16px; text-align: center; font-size: 15px; line-height: 1; }
|
||||||
|
.extpop .epit .lbl { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.urlwrap:focus-within { border-color: #4b7bec; }
|
.urlwrap:focus-within { border-color: #4b7bec; }
|
||||||
/* Site security shield — always present. Colour communicates state:
|
/* Site security shield — always present. Colour communicates state:
|
||||||
neutral (default) for home / resolving, green when the connection is
|
neutral (default) for home / resolving, green when the connection is
|
||||||
|
|
@ -312,6 +350,7 @@
|
||||||
</button>
|
</button>
|
||||||
<span class="extdock" id="extdock" hidden>
|
<span class="extdock" id="extdock" hidden>
|
||||||
<span id="extbuttons"></span>
|
<span id="extbuttons"></span>
|
||||||
|
<button class="extmore" id="extmore" title="Extensions" aria-label="Extensions">🧩</button>
|
||||||
</span>
|
</span>
|
||||||
<span class="upchip" id="upchip" hidden><button class="upcore" id="upDownload" title=""></button><button class="updismiss" id="upDismiss" title="Not now">✕</button></span>
|
<span class="upchip" id="upchip" hidden><button class="upcore" id="upDownload" title=""></button><button class="updismiss" id="upDismiss" title="Not now">✕</button></span>
|
||||||
<button class="tor" id="tor" title="Route traffic through Tor">🧅 Tor: Off</button>
|
<button class="tor" id="tor" title="Route traffic through Tor">🧅 Tor: Off</button>
|
||||||
|
|
@ -468,10 +507,72 @@
|
||||||
else { T.openSidebar && T.openSidebar(pid); }
|
else { T.openSidebar && T.openSidebar(pid); }
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
// Reflect active state on the collapsed .extmore button too.
|
||||||
|
$("extmore").classList.toggle("active", sidebarOpen && !!activePanelId);
|
||||||
}
|
}
|
||||||
T.sidebarState && T.sidebarState().then(renderSidebarState);
|
T.sidebarState && T.sidebarState().then(renderSidebarState);
|
||||||
T.onSidebarState && T.onSidebarState(renderSidebarState);
|
T.onSidebarState && T.onSidebarState(renderSidebarState);
|
||||||
|
|
||||||
|
// Collapsed extensions dropdown — opens under the .extmore button at
|
||||||
|
// responsive level 3, lists every registered panel with icon + name.
|
||||||
|
function closeExtPop() { const p = document.getElementById("extpop"); if (p) p.remove(); }
|
||||||
|
$("extmore").onclick = (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (document.getElementById("extpop")) return closeExtPop();
|
||||||
|
const rect = $("extmore").getBoundingClientRect();
|
||||||
|
const pop = document.createElement("div");
|
||||||
|
pop.id = "extpop"; pop.className = "extpop";
|
||||||
|
pop.innerHTML = lastPanels.map((p) => {
|
||||||
|
const isActive = sidebarOpen && p.panelId === activePanelId;
|
||||||
|
return `<div class="epit${isActive ? " active" : ""}" data-panel="${p.panelId.replace(/"/g,""")}"><span class="ic">${String(p.icon || "•").replace(/</g,"<")}</span><span class="lbl">${(p.title || "Extension").replace(/</g,"<")}</span></div>`;
|
||||||
|
}).join("") || `<div class="epit" style="opacity:.6;cursor:default">No extensions installed</div>`;
|
||||||
|
document.body.appendChild(pop);
|
||||||
|
const r = pop.getBoundingClientRect();
|
||||||
|
pop.style.left = Math.max(4, Math.min(rect.left, window.innerWidth - r.width - 4)) + "px";
|
||||||
|
pop.style.top = Math.round(rect.bottom + 4) + "px";
|
||||||
|
pop.querySelectorAll(".epit[data-panel]").forEach((row) => {
|
||||||
|
row.onclick = () => {
|
||||||
|
const pid = row.dataset.panel;
|
||||||
|
closeExtPop();
|
||||||
|
if (sidebarOpen && pid === activePanelId) { T.closeSidebar && T.closeSidebar(); }
|
||||||
|
else { T.openSidebar && T.openSidebar(pid); }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
const off = (e) => { if (!pop.contains(e.target) && e.target !== $("extmore")) { closeExtPop(); document.removeEventListener("mousedown", off); } };
|
||||||
|
document.addEventListener("mousedown", off);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Responsive breakpoints on .bar width. ResizeObserver reacts to window
|
||||||
|
// resize, sidebar toggle, and any chrome-height change — all cheap since
|
||||||
|
// we only touch a single dataset attribute if the level changes.
|
||||||
|
const barEl = $("bar");
|
||||||
|
const RESPONSIVE_BREAKS = [
|
||||||
|
{ w: 1050, level: "0" }, // wide
|
||||||
|
{ w: 820, level: "1" }, // hide search
|
||||||
|
{ w: 620, level: "2" }, // theseus icon-only
|
||||||
|
{ w: 0, level: "3" }, // extensions → dropdown
|
||||||
|
];
|
||||||
|
function pickResponsive(w) {
|
||||||
|
for (const b of RESPONSIVE_BREAKS) if (w >= b.w) return b.level;
|
||||||
|
return "3";
|
||||||
|
}
|
||||||
|
const ro = new ResizeObserver((entries) => {
|
||||||
|
for (const e of entries) {
|
||||||
|
const level = pickResponsive(e.contentRect.width);
|
||||||
|
if (barEl.dataset.responsive !== level) {
|
||||||
|
barEl.dataset.responsive = level;
|
||||||
|
// Close the extmore dropdown if we shrank/grew across level 3.
|
||||||
|
if (level !== "3") closeExtPop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ro.observe(barEl);
|
||||||
|
// 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);
|
||||||
|
|
||||||
// ---- favorites bar (new-tab page only) ----
|
// ---- favorites bar (new-tab page only) ----
|
||||||
let current = { url: "", title: "", favicon: null }, bookmarks = [];
|
let current = { url: "", title: "", favicon: null }, bookmarks = [];
|
||||||
function renderBookmarks() {
|
function renderBookmarks() {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue