The AI picker is now a sibling of the AI launch button in the sidebar strip, not a chevron badge glued to its corner. Launch button still opens the current engine in the left panel on click (right-click still opens the picker, kept as a power-user shortcut). The new pick button is a full-size strip icon with a three-row list glyph and its own tooltip; keyboard Enter / Space activates it. The Claude connector chip in the toolbar gained max-width + ellipsis on its label, plus flex-shrink wiring, so the Claude 3 tabs string never grows past 160 px and the search box keeps its text visible even when the connector is actively streaming.
219 lines
12 KiB
HTML
219 lines
12 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: 26px; height: 26px; 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; }
|
||
.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; }
|
||
/* Separate "Pick AI engine" button — a sibling of the launch button
|
||
so the two affordances read as distinct. Dot-grid glyph conveys
|
||
"picker" without copying the chevron's old meaning. */
|
||
.llmpick { color: var(--mut); margin-bottom: 2px; }
|
||
.llmpick:hover { color: var(--acid); }
|
||
.llmpick svg { width: 14px; height: 14px; display: block; }
|
||
</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>
|
||
<button class="icon llmpick" id="llmPick" hidden title="Pick AI engine" aria-label="Pick AI engine"><svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><circle cx="3" cy="4" r="1.4"/><circle cx="3" cy="8" r="1.4"/><circle cx="3" cy="12" r="1.4"/><rect x="6" y="3.2" width="8" height="1.6" rx=".8"/><rect x="6" y="7.2" width="8" height="1.6" rx=".8"/><rect x="6" y="11.2" width="8" height="1.6" rx=".8"/></svg><span class="tt">Pick AI</span></button>
|
||
<button class="icon gear" id="gearBtn" title="Sidebar settings"><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 settings</span></button>
|
||
</div>
|
||
<script src="strip-icons.js"></script>
|
||
<script>
|
||
const Q = window.quicklinks;
|
||
const icons = document.getElementById("icons");
|
||
// Gear: click opens Settings › Sidebar directly (no popover). The "+" add
|
||
// button that used to live below the gear is gone — adding a web app goes
|
||
// through Settings › Sidebar now, which the gear opens anyway.
|
||
const gearBtn = document.getElementById("gearBtn");
|
||
gearBtn.onclick = () => Q && Q.add && Q.add();
|
||
// 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 llmPick = document.getElementById("llmPick");
|
||
const llmGlyph = document.getElementById("llmGlyph");
|
||
const llmTip = document.getElementById("llmTip");
|
||
// Launch button: opens (or toggles) the current AI engine in the panel.
|
||
llmBtn.addEventListener("click", () => { Q && Q.open && Q.open("llm"); });
|
||
// Right-click on the launch button still opens the picker (power-user shortcut).
|
||
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 });
|
||
});
|
||
// Pick button: a sibling icon that opens the picker overlay. Anchored at
|
||
// the pick button so the overlay sits beside it.
|
||
llmPick.addEventListener("click", () => {
|
||
const r = llmPick.getBoundingClientRect();
|
||
Q && Q.llmMenu && Q.llmMenu({ x: r.left, y: r.top, width: r.width, height: r.height });
|
||
});
|
||
llmPick.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); const r = llmPick.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;
|
||
llmPick.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;
|
||
llmPick.hidden = true;
|
||
}
|
||
}
|
||
Q && Q.getState && Q.getState().then(render).catch(() => render(null));
|
||
Q && Q.onState && Q.onState(render);
|
||
</script>
|
||
</body>
|
||
</html>
|