The gear popover at the bottom of the left strip is reduced to a single "Open sidebar settings" item; the overlay/push radio and "Reset panel widths" controls move to Settings › Sidebar permanently, so there's one source of truth for the sidebar's own options. A new sparkle button is added below the icon stack: it opens the user's default AI engine (any search engine with kind=llm — Perplexity ships on by default now; Phind is in the catalog). Click opens the engine in the left panel like the existing web-app quick-links; right-click (or the small chevron in the button's corner) pops a native menu of every enabled LLM, each one-click to switch default + open. The button hides itself when no LLM engine is enabled. Hermes consolidation. The hermes.x web quick-link added in 0.3.99 is retracted — the bundled Hermes extension already registers a left-strip panel (side: "left"), so the web link was duplicating it. Any profile that caught 0.3.99 gets the stray entry yanked on first load. Ctrl+Shift+E now opens the Hermes panel in the active window's left sidebar instead of the standalone Messages BrowserWindow; the window is still reachable via the ⧉ pop-out button in the panel header for users who want a free-floating client. The home-page Silent Mode card for Hermes.X comes back (it's a link to the web page, not a shortcut to the panel — different purpose).
214 lines
11 KiB
HTML
214 lines
11 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head><meta charset="utf-8">
|
|
<style>
|
|
:root {
|
|
color-scheme: light dark;
|
|
--bg:#0e131c; --ink:#e7eaf1; --mut:#8b98a9; --line2:rgba(255,255,255,.13);
|
|
--acid:#d6ff3d; --acid-tint: rgb(from var(--acid) r g b / .14);
|
|
}
|
|
@media (prefers-color-scheme: light) {
|
|
:root { --bg:#eceff3; --ink:#1a1f28; --mut:#5c6676; --line2:rgba(0,0,0,.10); --acid:#0AC18E; --acid-tint: rgb(from var(--acid) r g b / .14); }
|
|
}
|
|
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
|
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; overflow: hidden;
|
|
user-select: none; }
|
|
/* Icons sit in a vertically-centered column: the two spacers split the
|
|
remaining space above and below so the stack reads as "quick access"
|
|
centered in the strip, Opera-style. */
|
|
.strip { display: flex; flex-direction: column; align-items: center;
|
|
padding: 6px 0; height: 100%; box-sizing: border-box;
|
|
border-right: 1px solid var(--line2); }
|
|
.icons { display: flex; flex-direction: column; align-items: center; gap: 4px;
|
|
flex: none; }
|
|
.spacer { flex: 1 1 auto; min-height: 10px; }
|
|
.icon { width: 34px; height: 34px; border-radius: 8px; display: grid; place-items: center;
|
|
cursor: pointer; background: transparent; border: none; padding: 0; color: var(--ink);
|
|
position: relative; }
|
|
.icon:hover { background: var(--line2); }
|
|
.icon:active { transform: scale(0.94); }
|
|
.icon.on { background: var(--acid-tint); box-shadow: inset 2px 0 0 var(--acid); }
|
|
.icon.dragging { opacity: .4; }
|
|
.icon.dropbefore { box-shadow: inset 0 2px 0 var(--acid); }
|
|
.icon.dropafter { box-shadow: inset 0 -2px 0 var(--acid); }
|
|
.icon svg, .icon img { width: 20px; height: 20px; display: block; }
|
|
.icon.letter { font: 600 13px/1 system-ui, sans-serif; color: var(--mut); }
|
|
.icon .tt { position: absolute; left: 44px; top: 50%; transform: translateY(-50%);
|
|
background: var(--bg); color: var(--ink); padding: 4px 8px; border-radius: 6px;
|
|
border: 1px solid var(--line2); white-space: nowrap; font-size: 12px;
|
|
pointer-events: none; opacity: 0; transition: opacity .1s; z-index: 10; }
|
|
.icon:hover .tt { opacity: 1; transition-delay: .4s; }
|
|
.add { color: var(--mut); font: 500 20px/1 system-ui, sans-serif; }
|
|
.sep { width: 22px; height: 1px; background: var(--line2); margin: 4px 0; }
|
|
.icon.emoji { font-size: 18px; }
|
|
.add:hover { color: var(--acid); }
|
|
/* Gear + LLM button sit at the bottom of the strip. The LLM button grows
|
|
a small chevron badge in the bottom-right that opens the "pick AI"
|
|
menu; the main area of the button opens the current default LLM in
|
|
the left panel. Right-click on the button is equivalent to the chevron. */
|
|
.gear { color: var(--mut); margin-bottom: 2px; }
|
|
.gear:hover { color: var(--ink); }
|
|
.gear svg { width: 17px; height: 17px; }
|
|
.llm { color: var(--mut); margin-bottom: 2px; position: relative; }
|
|
.llm:hover { color: var(--ink); }
|
|
.llm.on { background: var(--acid-tint); box-shadow: inset 2px 0 0 var(--acid); color: var(--ink); }
|
|
.llm .glyph { font-size: 16px; line-height: 1; }
|
|
.llm img { width: 18px; height: 18px; display: block; }
|
|
.llm .chev { position: absolute; right: 1px; bottom: 1px; width: 10px; height: 10px;
|
|
display: grid; place-items: center; color: var(--mut); background: var(--bg);
|
|
border: 1px solid var(--line2); border-radius: 3px; font-size: 7px; line-height: 1; padding: 0; cursor: pointer; }
|
|
.llm .chev:hover { color: var(--ink); }
|
|
</style></head>
|
|
<body>
|
|
<div class="strip">
|
|
<div class="spacer"></div>
|
|
<div class="icons" id="icons"></div>
|
|
<div class="spacer"></div>
|
|
<button class="icon llm" id="llmBtn" hidden><span class="glyph" id="llmGlyph">✨</span><span class="tt" id="llmTip">AI</span><button class="chev" id="llmChev" title="Pick AI engine">▾</button></button>
|
|
<button class="icon gear" id="gearBtn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg><span class="tt">Sidebar options</span></button>
|
|
<button class="icon add" id="addBtn" title="Add link">+<span class="tt">Add link</span></button>
|
|
</div>
|
|
<script src="strip-icons.js"></script>
|
|
<script>
|
|
const Q = window.quicklinks;
|
|
const icons = document.getElementById("icons");
|
|
const addBtn = document.getElementById("addBtn");
|
|
addBtn.onclick = () => Q && Q.add();
|
|
const gearBtn = document.getElementById("gearBtn");
|
|
gearBtn.onclick = () => {
|
|
const r = gearBtn.getBoundingClientRect();
|
|
Q && Q.menu && Q.menu({ x: r.left, y: r.top, width: r.width, height: r.height });
|
|
};
|
|
// LLM button: click opens the current default LLM in the left panel.
|
|
// Right-click (or the small chevron badge) opens the AI picker menu.
|
|
const llmBtn = document.getElementById("llmBtn");
|
|
const llmGlyph = document.getElementById("llmGlyph");
|
|
const llmTip = document.getElementById("llmTip");
|
|
const llmChev = document.getElementById("llmChev");
|
|
llmBtn.addEventListener("click", (e) => {
|
|
if (e.target === llmChev) return; // chevron handles its own click
|
|
Q && Q.open && Q.open("llm");
|
|
});
|
|
llmBtn.addEventListener("contextmenu", (e) => {
|
|
e.preventDefault();
|
|
const r = llmBtn.getBoundingClientRect();
|
|
Q && Q.llmMenu && Q.llmMenu({ x: r.left, y: r.top, width: r.width, height: r.height });
|
|
});
|
|
llmChev.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
const r = llmBtn.getBoundingClientRect();
|
|
Q && Q.llmMenu && Q.llmMenu({ x: r.left, y: r.top, width: r.width, height: r.height });
|
|
});
|
|
const { BRAND, svgFor: brandForLink, hostOf } = window.StripIcons;
|
|
function letterIcon(b, L) { b.classList.add("letter"); b.prepend(document.createTextNode((L.title || "?").slice(0, 1).toUpperCase())); }
|
|
// One order for web apps and extension panels (main keeps it); drag an
|
|
// icon up or down the strip to move it.
|
|
let order = [];
|
|
let dragKey = null;
|
|
function wireDrag(b, key) {
|
|
b.draggable = true;
|
|
b.dataset.key = key;
|
|
b.addEventListener("dragstart", (e) => { dragKey = key; b.classList.add("dragging"); try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("application/x-theseus-strip", key); } catch {} });
|
|
b.addEventListener("dragend", () => { dragKey = null; b.classList.remove("dragging"); icons.querySelectorAll(".icon").forEach((x) => x.classList.remove("dropbefore", "dropafter")); });
|
|
b.addEventListener("dragover", (e) => {
|
|
if (!dragKey || dragKey === key) return;
|
|
e.preventDefault(); e.dataTransfer.dropEffect = "move";
|
|
const r = b.getBoundingClientRect(); const before = (e.clientY - r.top) < r.height / 2;
|
|
b.classList.toggle("dropbefore", before); b.classList.toggle("dropafter", !before);
|
|
});
|
|
b.addEventListener("dragleave", () => b.classList.remove("dropbefore", "dropafter"));
|
|
b.addEventListener("drop", (e) => {
|
|
e.preventDefault();
|
|
if (!dragKey || dragKey === key) return;
|
|
const r = b.getBoundingClientRect(); const before = (e.clientY - r.top) < r.height / 2;
|
|
const next = order.filter((k) => k !== dragKey);
|
|
next.splice(next.indexOf(key) + (before ? 0 : 1), 0, dragKey);
|
|
dragKey = null;
|
|
Q && Q.reorder && Q.reorder(next);
|
|
});
|
|
}
|
|
function render(data) {
|
|
const links = (data && data.links) || [];
|
|
const openId = (data && data.openId) || null;
|
|
icons.innerHTML = "";
|
|
const panels = (data && data.panels) || [];
|
|
order = Array.isArray(data && data.order) ? data.order : [...panels.map((P) => P.id), ...links.map((L) => "q:" + L.id)];
|
|
const made = new Map();
|
|
for (const P of panels) {
|
|
const b = document.createElement("button");
|
|
b.className = "icon";
|
|
if (openId === P.id) b.classList.add("on");
|
|
b.title = P.title;
|
|
if (/^data:image\//.test(P.icon)) {
|
|
const img = document.createElement("img");
|
|
img.src = P.icon;
|
|
img.alt = "";
|
|
b.appendChild(img);
|
|
} else {
|
|
b.classList.add(P.icon ? "emoji" : "letter");
|
|
b.textContent = P.icon || (P.title || "?").slice(0, 1).toUpperCase();
|
|
}
|
|
const tt = document.createElement("span");
|
|
tt.className = "tt"; tt.textContent = P.title;
|
|
b.appendChild(tt);
|
|
b.onclick = () => Q && Q.open(P.id);
|
|
wireDrag(b, P.id);
|
|
made.set(P.id, b);
|
|
}
|
|
for (const L of links) {
|
|
const b = document.createElement("button");
|
|
b.className = "icon";
|
|
if (openId && L.id === openId) b.classList.add("on");
|
|
b.title = L.title || L.url;
|
|
const svg = brandForLink(L);
|
|
const host = hostOf(L.url);
|
|
if (svg) {
|
|
b.innerHTML = svg;
|
|
} else if (host && /^https?:/.test(L.url)) {
|
|
const img = document.createElement("img");
|
|
img.alt = "";
|
|
img.onerror = () => { img.remove(); letterIcon(b, L); };
|
|
img.src = new URL("/favicon.ico", L.url).href;
|
|
b.appendChild(img);
|
|
} else {
|
|
letterIcon(b, L);
|
|
}
|
|
const tt = document.createElement("span");
|
|
tt.className = "tt"; tt.textContent = L.title || L.url;
|
|
b.appendChild(tt);
|
|
b.onclick = () => Q && Q.open(L.id);
|
|
wireDrag(b, "q:" + L.id);
|
|
made.set("q:" + L.id, b);
|
|
}
|
|
for (const k of order) { const b = made.get(k); if (b) { icons.appendChild(b); made.delete(k); } }
|
|
for (const b of made.values()) icons.appendChild(b);
|
|
|
|
// LLM button — visible only when at least one LLM engine is enabled.
|
|
const llm = (data && data.llm) || null;
|
|
const engines = (llm && llm.engines) || [];
|
|
if (engines.length) {
|
|
llmBtn.hidden = false;
|
|
llmBtn.classList.toggle("on", !!(llm && llm.open));
|
|
const curId = (llm && llm.current) || engines[0].id;
|
|
const cur = engines.find((e) => e.id === curId) || engines[0];
|
|
llmTip.textContent = cur.name;
|
|
// Prefer the engine's bundled favicon; fall back to the symbol emoji.
|
|
llmGlyph.textContent = "";
|
|
llmGlyph.innerHTML = "";
|
|
if (cur.favicon) {
|
|
const img = document.createElement("img");
|
|
img.src = cur.favicon; img.alt = "";
|
|
img.onerror = () => { llmGlyph.innerHTML = ""; llmGlyph.textContent = cur.sym || "✨"; };
|
|
llmGlyph.appendChild(img);
|
|
} else {
|
|
llmGlyph.textContent = cur.sym || "✨";
|
|
}
|
|
} else {
|
|
llmBtn.hidden = true;
|
|
}
|
|
}
|
|
Q && Q.getState && Q.getState().then(render).catch(() => render(null));
|
|
Q && Q.onState && Q.onState(render);
|
|
</script>
|
|
</body>
|
|
</html>
|