From f36e1db17f467e2b7a777d58be05ceb46ec5eb35 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 6 Sep 2026 21:45:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(theseus/chrome):=20adaptive=20toolbar=20?= =?UTF-8?q?=E2=80=94=20drops=20search,=20collapses=20Theseus,=20folds=20ex?= =?UTF-8?q?tension=20dock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- chrome.html | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/chrome.html b/chrome.html index cb93edd..04fb4d4 100644 --- a/chrome.html +++ b/chrome.html @@ -154,6 +154,44 @@ .bar[data-searchsize="hidden"] .searchbox { display: none; } .bar[data-searchsize="compact"] .searchbox { width: 180px; } .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; } /* Site security shield — always present. Colour communicates state: neutral (default) for home / resolving, green when the connection is @@ -312,6 +350,7 @@ @@ -468,10 +507,72 @@ 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.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 `
${String(p.icon || "•").replace(/${(p.title || "Extension").replace(/
`; + }).join("") || `
No extensions installed
`; + 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) ---- let current = { url: "", title: "", favicon: null }, bookmarks = []; function renderBookmarks() {