theseus/quicklinks.html
Local Dev 9095a5ecf9 Theseus 0.3.112: AI picker chevron fixed, home polish, performance toggles
Three home-page tweaks:
- Removed the YOUR LINKS and SILENT MODE section headings; the two
  panels now stand on their own without labels.
- The + Add card cell now matches the real cards' size and shape so
  it reads as a peer of the grid rather than a short dashed strip.
- Empty slot cells in the left grid are invisible by default and
  only reveal their dashed outline when you hover the strip or drag
  a link over them. The + add-link cell stays visible as the
  discoverable entry.

AI engine picker:
- The chevron badge on the AI button is now a plain span with the
  button role, not a nested <button> inside the main AI button.
  Chromium flattened the invalid nesting so the chevron's own click
  handler never fired; the picker now opens from the chevron and
  from keyboard Enter / Space as well as right-click.

Performance (Settings > Performance > Startup):
- Pithos ships with activation: on-demand; its control server for
  s3d no longer boots at launch. Wakes on first panel open or a
  pithos.sia navigation.
- Hermes relay auto-restore (1.5 s after launch) is now gated by a
  new setting hermesAutoRestore (default on). Turn it off to skip
  background Nostr relay connections for users who don't live in
  Hermes.
- New setting pithosAtLaunch (default off) mirrors the existing
  walletAtLaunch so users who do want Pithos ready at launch can
  opt back in.
- The Lightest preset now also turns off pithosAtLaunch and
  hermesAutoRestore.
2026-10-24 14:20:20 +03:00

214 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: 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; 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><span class="chev" id="llmChev" role="button" tabindex="0" aria-label="Pick AI engine" title="Pick AI engine">▾</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 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("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); 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>