An open sidebar panel (a web app or an extension panel) now has a bar on top with its name, Maximize/Restore (it covers the page, up to the right sidebar) and Close. How panels sit next to the page is a choice, in the new gear above + on the strip and in Settings > Sidebar: over the page, where it floats on the left of a page that keeps its width and a click in the page closes it; or pushing the page aside, where it stays open until closed. That choice replaces the pin, which only existed to keep a panel open on a page click. Also: a window laid out while minimized reads a zero-size area, so every view was left at 0x0 and the window came back blank until resized. Layout now skips a minimized window and runs again on restore.
133 lines
9.5 KiB
HTML
133 lines
9.5 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); }
|
||
/* Gear: how panels sit next to the page, and the Sidebar settings. */
|
||
.gear { color: var(--mut); margin-bottom: 2px; }
|
||
.gear:hover { color: var(--ink); }
|
||
.gear svg { width: 17px; height: 17px; }
|
||
</style></head>
|
||
<body>
|
||
<div class="strip">
|
||
<div class="spacer"></div>
|
||
<div class="icons" id="icons"></div>
|
||
<div class="spacer"></div>
|
||
<button class="icon gear" id="gearBtn"><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 options</span></button>
|
||
<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();
|
||
const gearBtn = document.getElementById("gearBtn");
|
||
gearBtn.onclick = () => {
|
||
const r = gearBtn.getBoundingClientRect();
|
||
Q && Q.menu && Q.menu({ x: r.left, y: r.top, width: r.width, height: r.height });
|
||
};
|
||
// 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>
|