Extensions could only put panels in the right sidebar. An add-on can now register a panel with side: "left": its icon joins the quick-links strip (above the web-app links), and it opens in the strip's panel slot in its own view with the sidebar preload, so the add-on bridge, events and the widen/narrow controls work as on the right. A left panel and a quick-link web app never share the slot; reopening keeps the panel's page and state. Left panels drop out of the right sidebar and its dock. With the strip turned off they fall back to the right sidebar so they stay reachable. Pithos is the first: bundled copy rebuilt with side: "left".
123 lines
8.3 KiB
HTML
123 lines
8.3 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 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); }
|
||
</style></head>
|
||
<body>
|
||
<div class="strip">
|
||
<div class="spacer"></div>
|
||
<div class="icons" id="icons"></div>
|
||
<div class="spacer"></div>
|
||
<button class="icon add" id="addBtn" title="Add link">+<span class="tt">Add link</span></button>
|
||
</div>
|
||
<script>
|
||
const Q = window.quicklinks;
|
||
const icons = document.getElementById("icons");
|
||
const addBtn = document.getElementById("addBtn");
|
||
addBtn.onclick = () => Q && Q.add();
|
||
// Bundled brand marks. Each SVG is tight (20×20 viewBox, no padding), the
|
||
// colour is baked in so icons read at a glance in both themes. Unknown ids
|
||
// fall back to a letter chip. All marks are purely to identify services.
|
||
const BRAND = {
|
||
x: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2H21.5l-7.63 8.72L22.5 22h-7.09l-5.54-7.26L3.5 22H.25l8.18-9.35L1.5 2h7.26l5.01 6.63L18.244 2zm-2.49 18.3h1.84L7.37 3.6H5.4l10.354 16.7z"/></svg>',
|
||
twitter: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2H21.5l-7.63 8.72L22.5 22h-7.09l-5.54-7.26L3.5 22H.25l8.18-9.35L1.5 2h7.26l5.01 6.63L18.244 2zm-2.49 18.3h1.84L7.37 3.6H5.4l10.354 16.7z"/></svg>',
|
||
telegram: '<svg viewBox="0 0 24 24" fill="#229ED9"><path d="M9.78 15.65 9.53 19.1c.37 0 .54-.16.73-.35l1.76-1.65 3.65 2.63c.67.37 1.15.18 1.33-.61l2.42-11.35h.01c.21-.98-.36-1.37-1.01-1.13L3.87 10.84c-.95.38-.94.92-.17 1.16l3.73 1.16L16 7.52c.4-.27.77-.12.47.15l-6.69 7.98z"/></svg>',
|
||
whatsapp: '<svg viewBox="0 0 24 24" fill="#25D366"><path d="M19.11 4.86A9.78 9.78 0 0 0 12.04 2 9.86 9.86 0 0 0 3.6 16.84L2.05 22l5.3-1.39A9.84 9.84 0 0 0 12.04 22h.01a9.86 9.86 0 0 0 9.85-9.86 9.78 9.78 0 0 0-2.79-7.28zM12.05 20.3h-.01a8.17 8.17 0 0 1-4.16-1.14l-.3-.18-3.14.82.84-3.06-.2-.32a8.2 8.2 0 1 1 15.19-4.3 8.2 8.2 0 0 1-8.22 8.18zm4.49-6.14c-.25-.12-1.46-.72-1.69-.8-.23-.09-.4-.12-.56.12-.17.25-.65.8-.8.97-.14.17-.29.19-.54.06-.25-.12-1.05-.39-2-1.23a7.5 7.5 0 0 1-1.38-1.72c-.14-.25-.02-.38.11-.5.11-.11.25-.29.37-.43.12-.14.17-.25.25-.42.08-.17.04-.31-.02-.43-.06-.12-.56-1.35-.77-1.85-.2-.49-.4-.42-.56-.43l-.47-.01c-.17 0-.43.06-.65.31-.22.25-.86.84-.86 2.05 0 1.21.88 2.38 1 2.55.12.17 1.74 2.65 4.21 3.72.59.25 1.05.4 1.41.52.59.19 1.13.16 1.56.1.47-.07 1.46-.6 1.66-1.18.21-.57.21-1.07.14-1.17-.06-.1-.23-.17-.48-.3z"/></svg>',
|
||
messenger: '<svg viewBox="0 0 24 24"><defs><linearGradient id="mg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#00B2FF"/><stop offset="1" stop-color="#006AFF"/></linearGradient></defs><path fill="url(#mg)" d="M12 2C6.48 2 2 6.14 2 11.26c0 2.77 1.3 5.24 3.37 6.95V22l3.08-1.69c.82.23 1.69.35 2.55.35 5.52 0 10-4.14 10-9.4C22 6.14 17.52 2 12 2zm1 12.63-2.56-2.72-4.99 2.72 5.49-5.84 2.62 2.72 4.93-2.72-5.49 5.84z"/></svg>',
|
||
youtube: '<svg viewBox="0 0 24 24" fill="#FF0000"><path d="M23.5 6.5a3 3 0 0 0-2.1-2.12C19.46 4 12 4 12 4s-7.46 0-9.4.38A3 3 0 0 0 .5 6.5 31.3 31.3 0 0 0 .11 12a31.3 31.3 0 0 0 .4 5.5 3 3 0 0 0 2.1 2.12C4.54 20 12 20 12 20s7.46 0 9.4-.38a3 3 0 0 0 2.1-2.12A31.3 31.3 0 0 0 23.9 12a31.3 31.3 0 0 0-.4-5.5zM9.75 15.57V8.43L15.82 12l-6.07 3.57z"/></svg>',
|
||
spotify: '<svg viewBox="0 0 24 24" fill="#1DB954"><path d="M12 0a12 12 0 1 0 0 24 12 12 0 0 0 0-24zm5.5 17.3a.75.75 0 0 1-1.03.25c-2.82-1.72-6.37-2.11-10.56-1.15a.75.75 0 1 1-.34-1.46c4.58-1.05 8.5-.6 11.68 1.33a.75.75 0 0 1 .25 1.03zm1.47-3.26a.94.94 0 0 1-1.29.3c-3.23-1.98-8.14-2.56-11.95-1.4a.94.94 0 1 1-.55-1.8c4.36-1.33 9.78-.68 13.5 1.6a.94.94 0 0 1 .3 1.3zm.13-3.4C15.26 8.4 8.72 8.2 5.1 9.34a1.12 1.12 0 1 1-.66-2.15c4.16-1.27 11.37-1.02 15.86 1.65a1.12 1.12 0 1 1-1.14 1.93z"/></svg>',
|
||
discord: '<svg viewBox="0 0 24 24" fill="#5865F2"><path d="M20.3 4.5A17.6 17.6 0 0 0 15.9 3l-.2.4a16.2 16.2 0 0 1 3.7 1.2 20 20 0 0 0-5.6-1 18.4 18.4 0 0 0-5.7 1A16.2 16.2 0 0 1 11.8 3.4L11.6 3a17.6 17.6 0 0 0-4.4 1.5A20.2 20.2 0 0 0 3.2 15.5a16.4 16.4 0 0 0 5 2.5l.6-1a11.3 11.3 0 0 1-2.9-1.4l.5-.3c3 1.5 9.1 1.5 12 0l.5.3c-.9.5-1.9 1-2.9 1.4l.6 1a16.4 16.4 0 0 0 5-2.5A20.2 20.2 0 0 0 20.3 4.5zM9 13c-1 0-1.8-1-1.8-2.2S8 8.6 9 8.6c1 0 1.8 1 1.8 2.2S10 13 9 13zm6 0c-1 0-1.8-1-1.8-2.2S14 8.6 15 8.6c1 0 1.8 1 1.8 2.2S16 13 15 13z"/></svg>',
|
||
};
|
||
function brandFor(id) { return BRAND[String(id || "").toLowerCase()] || null; }
|
||
function render(data) {
|
||
const links = (data && data.links) || [];
|
||
const openId = (data && data.openId) || null;
|
||
icons.innerHTML = "";
|
||
// Extension panels (registered with side: "left") come first.
|
||
const panels = (data && data.panels) || [];
|
||
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);
|
||
icons.appendChild(b);
|
||
}
|
||
if (panels.length && links.length) {
|
||
const sep = document.createElement("div");
|
||
sep.className = "sep";
|
||
icons.appendChild(sep);
|
||
}
|
||
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 = brandFor(L.id);
|
||
if (svg) {
|
||
b.innerHTML = svg;
|
||
} else {
|
||
b.classList.add("letter");
|
||
b.textContent = (L.title || "?").slice(0, 1).toUpperCase();
|
||
}
|
||
const tt = document.createElement("span");
|
||
tt.className = "tt"; tt.textContent = L.title || L.url;
|
||
b.appendChild(tt);
|
||
b.onclick = () => Q && Q.open(L.id);
|
||
icons.appendChild(b);
|
||
}
|
||
}
|
||
Q && Q.getState && Q.getState().then(render).catch(() => render(null));
|
||
Q && Q.onState && Q.onState(render);
|
||
</script>
|
||
</body>
|
||
</html>
|