Clicking an icon on the left strip now opens the service inside a dedicated 380-px mini-view (quickPanel) next to the strip, Opera-style, instead of a new full-sized tab. Click the active icon again to close the panel; click a different one to switch. If the panel is already pointed at the same host, we skip the loadURL so scroll position, open chat and login state survive a close+reopen round-trip. Icons now paint as real brand SVGs (Messenger, WhatsApp, Telegram, X, YouTube, Spotify) with their official colours, bundled inside quicklinks.html so no external favicon fetch leaks the fact that the strip is loaded. Unknown ids fall back to a letter chip. The strip vertically centres the icons between two flex spacers to match Opera's layout. Defaults ship Messenger, WhatsApp, Telegram, X, YouTube and Spotify. The Settings › General › Quick links section still lists / adds / removes entries and toggles the strip.
94 lines
7.2 KiB
HTML
94 lines
7.2 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; }
|
||
.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 = "";
|
||
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>
|