theseus/quicklinks.html
Local Dev 13d4ab76b3 Theseus 0.3.101: Grok + Gemini + ChatGPT, sidebar polish
Three more LLM engines land in the catalog and in DEFAULT_ENABLED, so the
sparkle button's picker lists five AI engines out of the box: Perplexity,
Phind, ChatGPT, Grok, Gemini. The first two answer a URL query without any
login, the latter three need a free-tier account; all three accept the
chat URL as the "open the home" target, which is what the sidebar button
needs. A one-time offer migration adds them to any existing profile's
enabledEngines (users who disable one keep it off across restarts via a
new `enginesOffered` field).

Sidebar strip polish. The gear button now opens Settings › Sidebar on a
single click instead of popping a one-item native menu first. The "+"
add button under the gear is removed — adding a web app goes through
Settings › Sidebar anyway, which the gear opens. The quicklinks-add IPC
is kept but repointed to Settings › Sidebar (it used to open General,
which was the wrong page for the add-a-web-app flow).
2026-10-21 21:55:10 +03:00

211 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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" 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 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>