Two tweaks so the mark reads at sidebar size. - addon.json/mark.svg (and every mirror of mark.svg across the sites): rebuild with ZERO padding around the content bbox so the icon fills the slot vertically — before it was centred in a square viewBox that wasted ~45% of pixels. Version bumped to 0.2.5 so Theseus re-seeds. - quicklinks.html: .icon svg, .icon img goes from 20×20 to 26×26 (container stays 34×34; same breathing room, bigger glyph). Ships with the next Theseus release — can't be hot-patched into the asar. Deploy state: site-hermes-x is live on the VPS and in bns/hermes.x/ via the silentmode-sia-sync systemd timer, which was extended to include hermes-x alongside sirius-x and bergamot. Future rsyncs of site-hermes-x/ to the VPS auto-sync to Sia within 5 minutes.
211 lines
11 KiB
HTML
211 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: 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><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>
|