The web app rows in Settings showed a letter where the strip shows the app's icon. The icons now live in strip-icons.js, shared by both. Typing the start of a well-known app's name or address in the add row shows the rest in grey, the address field offers that app's address, and the right arrow takes it.
157 lines
8 KiB
HTML
157 lines
8 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: 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 src="strip-icons.js"></script>
|
|
<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 });
|
|
};
|
|
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);
|
|
}
|
|
Q && Q.getState && Q.getState().then(render).catch(() => render(null));
|
|
Q && Q.onState && Q.onState(render);
|
|
</script>
|
|
</body>
|
|
</html>
|