Theseus: Settings › Sidebar shows the apps' icons and suggests well-known ones

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.
This commit is contained in:
Local Dev 2026-10-08 01:38:53 +02:00
parent df685d315a
commit 1ad2b5d647
4 changed files with 131 additions and 33 deletions

View file

@ -102,6 +102,7 @@
"js-dialog.html",
"js-dialog-preload.js",
"quicklinks.html",
"strip-icons.js",
"quicklinks-preload.js",
"panel-grip-preload.js",
"panel-header.html",

View file

@ -55,6 +55,7 @@
<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");
@ -65,38 +66,7 @@
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>',
// Proton Mail: an envelope in Proton's purple.
proton: '<svg viewBox="0 0 24 24" fill="none"><rect x="2" y="5" width="20" height="14" rx="3" fill="#6D4AFF"/><path d="M3.5 7.5 12 13.2l8.5-5.7" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></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; }
// A link added by hand gets an id like "youtube-k3f9", so the mark is also
// found by the site's address.
const HOST_BRAND = [
[/(^|\.)(x|twitter)\.com$/, "x"], [/(^|\.)telegram\.org$/, "telegram"], [/(^|\.)whatsapp\.com$/, "whatsapp"],
[/(^|\.)messenger\.com$/, "messenger"], [/(^|\.)(youtube\.com|youtu\.be)$/, "youtube"], [/(^|\.)spotify\.com$/, "spotify"],
[/(^|\.)discord(app)?\.(com|gg)$/, "discord"], [/(^|\.)(proton\.me|protonmail\.com)$/, "proton"],
];
const hostOf = (url) => { try { return new URL(url).hostname.toLowerCase(); } catch { return ""; } };
function brandForLink(L) {
const byId = brandFor(L.id);
if (byId) return byId;
const h = hostOf(L.url);
for (const [re, key] of HOST_BRAND) if (re.test(h)) return BRAND[key];
return null;
}
// Any other site: its own favicon, and the first letter if it has none.
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.

View file

@ -261,6 +261,8 @@
}
/* PIN setup dialog — the same pad as the unlock prompt and Aegis. */
.pinwiz{position:fixed;inset:0;background:rgba(0,0,0,.5);display:grid;place-items:center;z-index:50;padding:16px}
/* Settings › Sidebar: a web app's icon, as on the strip. */
.qlico svg, .qlico img{width:18px;height:18px;display:block}
/* display:grid / display:flex would otherwise beat the hidden attribute */
.pinwiz[hidden], .row[hidden]{display:none}
.pinwiz-box{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px 22px 16px;width:min(400px,100%);box-shadow:0 20px 60px #000c}
@ -1015,6 +1017,7 @@
</section>
</div>
</div>
<script src="strip-icons.js"></script>
<script>
const C = window.cfg;
// Shared language picker list (same as WEBSITE_LANGUAGE_QUICK in main.js and
@ -1319,7 +1322,7 @@
row.draggable = true;
row.dataset.id = L.id;
row.style.cssText = "display:flex;align-items:center;gap:8px;flex-wrap:wrap;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:8px 12px";
row.innerHTML = `<span class="grip" title="Drag to reorder">⠿</span><div style="width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,.06);display:grid;place-items:center;font-weight:600;color:var(--acid-text);flex:none">${esc((L.title || "?").slice(0,1).toUpperCase())}</div>
row.innerHTML = `<span class="grip" title="Drag to reorder">⠿</span>${iconBox(L)}
<div style="flex:1;min-width:140px"><div style="font-weight:600">${esc(L.title || L.url)}</div><div style="font-size:12px;color:var(--mut);word-break:break-all">${esc(L.url)}${widths[key] ? " · " + Number(widths[key]) + " px" : ""}</div></div>
${widthBtnHtml(widths[key], key)}
<button class="btn ghost" data-del="${esc(L.id)}">Remove</button>`;
@ -1348,6 +1351,18 @@
});
});
wireWidth(list);
// A site without a bundled icon whose favicon does not load: its letter.
list.querySelectorAll("img[data-fav]").forEach((img) => { img.onerror = () => { const d = img.parentElement; img.remove(); d.textContent = d.dataset.letter || "?"; }; });
}
// The same icon the strip shows: the bundled mark, else the site's
// favicon, else the first letter of the title.
function iconBox(L) {
const box = "width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,.06);display:grid;place-items:center;font-weight:600;color:var(--acid-text);flex:none";
const svg = window.StripIcons && window.StripIcons.svgFor(L);
if (svg) return `<div class="qlico" style="${box}">${svg}</div>`;
const letter = esc((L.title || "?").slice(0, 1).toUpperCase());
let fav = ""; try { if (/^https?:/i.test(L.url)) fav = new URL("/favicon.ico", L.url).href; } catch {}
return `<div class="qlico" style="${box}" data-letter="${letter}">${fav ? `<img data-fav src="${esc(fav)}" alt="">` : letter}</div>`;
}
// Shared with the extension-panel list below.
window.__sbWireWidth = wireWidth;
@ -1360,6 +1375,64 @@
}
window.__sbButtons = { widthBtnHtml };
render(s.quickLinks || []);
// Typing a well-known app's name shows the rest of it in grey, and its
// address in the address field; → at the end of a field takes the grey
// part. An address can be started without "https://" too.
{
const KNOWN = (window.StripIcons && window.StripIcons.KNOWN) || [];
const urlPlaceholder = urlIn.placeholder;
const titleSuggestion = () => {
const v = titleIn.value.trim().toLowerCase();
if (!v) return "";
const k = KNOWN.find((x) => x.title.toLowerCase().startsWith(v));
return k ? k.title : "";
};
const urlSuggestion = () => {
const t = titleIn.value.trim().toLowerCase();
const byTitle = KNOWN.find((x) => x.title.toLowerCase() === t);
const v = urlIn.value.trim().toLowerCase();
if (!v) return byTitle ? byTitle.url : "";
const forms = (u) => [u, u.replace(/^https:\/\//, ""), u.replace(/^https:\/\/(www\.)?/, "")];
for (const k of byTitle ? [byTitle, ...KNOWN] : KNOWN) for (const f of forms(k.url)) if (f.toLowerCase().startsWith(v)) return f;
return "";
};
function suggest(input, get) {
const wrap = document.createElement("span");
wrap.style.cssText = `position:relative;display:flex;flex:${input.style.flex || "1"};min-width:${input.style.minWidth || "0"}`;
input.parentNode.insertBefore(wrap, input);
wrap.appendChild(input);
input.style.flex = "1"; input.style.minWidth = "0"; input.style.width = "100%"; input.style.boxSizing = "border-box";
const ghost = document.createElement("span");
wrap.appendChild(ghost);
const sync = () => {
const cs = getComputedStyle(input);
ghost.style.cssText = `position:absolute;inset:0;pointer-events:none;white-space:pre;overflow:hidden;display:flex;align-items:center;box-sizing:border-box;font:${cs.font};padding:${cs.padding};border:${cs.borderTopWidth} solid transparent`;
const sug = get(), v = input.value;
const fits = sug && sug.toLowerCase().startsWith(v.toLowerCase()) && sug.length > v.length;
input.dataset.suggest = fits ? sug : "";
ghost.textContent = "";
if (fits && v) {
const typed = document.createElement("span"); typed.style.color = "transparent"; typed.textContent = v;
const rest = document.createElement("span"); rest.style.color = "var(--dim)"; rest.textContent = sug.slice(v.length);
ghost.append(typed, rest);
}
};
input.addEventListener("input", sync);
input.addEventListener("keydown", (e) => {
if (e.key !== "ArrowRight" || !input.dataset.suggest) return;
if (input.selectionStart !== input.value.length || input.selectionEnd !== input.value.length) return;
e.preventDefault();
input.value = input.dataset.suggest;
input.dispatchEvent(new Event("input"));
});
return sync;
}
const syncUrl = suggest(urlIn, urlSuggestion);
const syncTitle = suggest(titleIn, titleSuggestion);
// The address field's grey hint is the matched app's address.
titleIn.addEventListener("input", () => { const u = urlSuggestion(); urlIn.placeholder = (!urlIn.value && u) ? u + " →" : urlPlaceholder; syncUrl(); });
syncTitle(); syncUrl();
}
addBtn.onclick = async () => {
const t = String(titleIn.value || "").trim();
const u = String(urlIn.value || "").trim();

54
strip-icons.js Normal file
View file

@ -0,0 +1,54 @@
// strip-icons.js — the sidebar web apps' icons and the well-known apps,
// shared by the strip (quicklinks.html) and Settings › Sidebar (settings.html)
// so both always show the same mark for a link.
"use strict";
(function () {
// 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>',
// Proton Mail: an envelope in Proton's purple.
proton: '<svg viewBox="0 0 24 24" fill="none"><rect x="2" y="5" width="20" height="14" rx="3" fill="#6D4AFF"/><path d="M3.5 7.5 12 13.2l8.5-5.7" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></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; }
// A link added by hand gets an id like "youtube-k3f9", so the mark is also
// found by the site's address.
const HOST_BRAND = [
[/(^|\.)(x|twitter)\.com$/, "x"], [/(^|\.)telegram\.org$/, "telegram"], [/(^|\.)whatsapp\.com$/, "whatsapp"],
[/(^|\.)messenger\.com$/, "messenger"], [/(^|\.)(youtube\.com|youtu\.be)$/, "youtube"], [/(^|\.)spotify\.com$/, "spotify"],
[/(^|\.)discord(app)?\.(com|gg)$/, "discord"], [/(^|\.)(proton\.me|protonmail\.com)$/, "proton"],
];
const hostOf = (url) => { try { return new URL(url).hostname.toLowerCase(); } catch { return ""; } };
function brandForLink(L) {
const byId = brandFor(L.id);
if (byId) return byId;
const h = hostOf(L.url);
for (const [re, key] of HOST_BRAND) if (re.test(h)) return BRAND[key];
return null;
}
// Any other site: its own favicon, and the first letter if it has none.
// Well-known web apps: what Settings › Sidebar suggests as you type a title
// or an address (→ accepts the grey rest of the suggestion).
const KNOWN = [
{ title: "Proton Mail", url: "https://mail.proton.me/" },
{ title: "Telegram", url: "https://web.telegram.org/k/" },
{ title: "WhatsApp", url: "https://web.whatsapp.com/" },
{ title: "X", url: "https://x.com/" },
{ title: "YouTube", url: "https://www.youtube.com/" },
{ title: "Discord", url: "https://discord.com/app" },
{ title: "Messenger", url: "https://www.messenger.com/" },
{ title: "Spotify", url: "https://open.spotify.com/" },
];
// The <svg> markup for a link, or null when it has no bundled mark.
function svgFor(L) { return brandForLink(L || {}); }
window.StripIcons = { BRAND, svgFor, hostOf, KNOWN };
})();