158 lines
7.6 KiB
HTML
158 lines
7.6 KiB
HTML
|
|
<!doctype html>
|
||
|
|
<html><head><meta charset="utf-8">
|
||
|
|
<style>
|
||
|
|
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
|
||
|
|
html, body { margin: 0; background: transparent; }
|
||
|
|
.menu { background: #1c222c; border: 1px solid #ffffff26; border-radius: 12px;
|
||
|
|
box-shadow: 0 16px 44px #000a, 0 2px 6px #0006; overflow: hidden;
|
||
|
|
color: #e7eaf1; min-width: 280px; }
|
||
|
|
.section { padding: 6px 0; }
|
||
|
|
.hdr { font-size: 10.5px; letter-spacing: .07em; text-transform: uppercase;
|
||
|
|
color: #8b98a9; padding: 10px 14px 6px; font-weight: 600; }
|
||
|
|
.item { display: flex; align-items: center; gap: 10px; padding: 9px 14px;
|
||
|
|
cursor: pointer; font-size: 13.5px; line-height: 1.3; user-select: none; }
|
||
|
|
.item:hover { background: #ffffff14; }
|
||
|
|
.item.active { background: rgba(214,255,61,.08); }
|
||
|
|
.item .ic { width: 20px; flex: none; text-align: center; }
|
||
|
|
.item .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
|
|
.item .sub { color: #8b98a9; font-size: 11.5px; margin-top: 1px; }
|
||
|
|
.item .wrap { flex: 1; min-width: 0; }
|
||
|
|
.item .chk { color: #d6ff3d; font-size: 15px; margin-left: auto; flex: none; }
|
||
|
|
.item.disabled { opacity: .45; cursor: default; }
|
||
|
|
.item.disabled:hover { background: transparent; }
|
||
|
|
.item.disabled .tag { font-size: 10.5px; color: #8b98a9; padding: 1px 6px;
|
||
|
|
border: 1px solid #ffffff22; border-radius: 10px;
|
||
|
|
margin-left: auto; flex: none; letter-spacing: .02em; }
|
||
|
|
.item.action { color: #d6ff3d; font-weight: 500; }
|
||
|
|
.item.action .ic { color: #d6ff3d; }
|
||
|
|
.sep { height: 1px; background: #ffffff16; margin: 2px 0; }
|
||
|
|
.more { padding: 7px 14px; font-size: 12px; color: #8b98a9; cursor: pointer;
|
||
|
|
display: flex; align-items: center; gap: 6px; user-select: none; }
|
||
|
|
.more:hover { color: #e7eaf1; }
|
||
|
|
.more .arrow { transition: transform .15s; display: inline-block; }
|
||
|
|
.more.open .arrow { transform: rotate(90deg); }
|
||
|
|
.more-list { max-height: 0; overflow: hidden; transition: max-height .18s ease; }
|
||
|
|
.more-list.open { max-height: 420px; overflow-y: auto; }
|
||
|
|
.foot { padding: 4px 0 6px; }
|
||
|
|
.foot .item { color: #b9c2d0; font-size: 12.5px; padding: 8px 14px; }
|
||
|
|
@media (prefers-color-scheme: light) {
|
||
|
|
.menu { background: #ffffff; border-color: rgba(0,0,0,.14); color: #1a1f28;
|
||
|
|
box-shadow: 0 14px 36px rgba(0,0,0,.18), 0 2px 6px rgba(0,0,0,.08); }
|
||
|
|
.hdr { color: #6b7584; } .item:hover { background: rgba(0,0,0,.055); }
|
||
|
|
.item.active { background: rgba(10,193,142,.09); }
|
||
|
|
.item .chk { color: #0ac18e; } .item.action { color: #0ac18e; }
|
||
|
|
.item.action .ic { color: #0ac18e; }
|
||
|
|
.item.disabled .tag { color: #6b7584; border-color: rgba(0,0,0,.14); }
|
||
|
|
.sep { background: rgba(0,0,0,.09); } .foot .item { color: #3c4453; }
|
||
|
|
.more { color: #6b7584; } .more:hover { color: #1a1f28; }
|
||
|
|
}
|
||
|
|
/* Thin scrollbar inside the expandable "More languages" area */
|
||
|
|
.more-list::-webkit-scrollbar { width: 6px; }
|
||
|
|
.more-list::-webkit-scrollbar-thumb { background: #ffffff22; border-radius: 3px; }
|
||
|
|
</style></head>
|
||
|
|
<body>
|
||
|
|
<div class="menu" id="menu">
|
||
|
|
<div id="actions"></div>
|
||
|
|
<div id="langs" class="section"></div>
|
||
|
|
<div class="sep"></div>
|
||
|
|
<div class="more" id="moreToggle" hidden>
|
||
|
|
<span class="arrow">▸</span><span>More languages (translator coming later)</span>
|
||
|
|
</div>
|
||
|
|
<div class="more-list" id="moreList"></div>
|
||
|
|
<div class="sep"></div>
|
||
|
|
<div class="foot">
|
||
|
|
<div class="item" id="openSettings">
|
||
|
|
<span class="ic">⚙</span><span class="nm">Language settings…</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<script>
|
||
|
|
const $ = (id) => document.getElementById(id);
|
||
|
|
const esc = (s) => String(s || "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">","\"":""","'":"'" })[c]);
|
||
|
|
function report() {
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
try { window.langpicker.resize($("menu").offsetHeight, $("menu").offsetWidth); } catch {}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
// One payload builds the whole menu. State lives in main; the picker just
|
||
|
|
// paints what it is given — a pick action calls back into main and the
|
||
|
|
// picker closes.
|
||
|
|
window.langpicker.onState((d) => {
|
||
|
|
const curTag = (d.currentTag || "").toLowerCase();
|
||
|
|
const isAuto = !!d.auto;
|
||
|
|
// Translate / Show-original actions at the top when applicable.
|
||
|
|
const actionRows = [];
|
||
|
|
if (d.translated) {
|
||
|
|
actionRows.push(`<div class="item action" data-act="revert">
|
||
|
|
<span class="ic">⟲</span><span class="nm">Show original (${esc(d.sourceName || "")})</span>
|
||
|
|
</div>`);
|
||
|
|
} else if (d.canTranslate) {
|
||
|
|
actionRows.push(`<div class="item action" data-act="translate">
|
||
|
|
<span class="ic">⇄</span>
|
||
|
|
<div class="wrap"><div>Translate this page</div><div class="sub">${esc(d.pageLangName || "")} → ${esc(d.targetName || "")}</div></div>
|
||
|
|
</div>`);
|
||
|
|
}
|
||
|
|
if (actionRows.length) actionRows.push(`<div class="sep"></div>`);
|
||
|
|
$("actions").innerHTML = actionRows.join("");
|
||
|
|
|
||
|
|
// Supported languages + Automatic. Current pin gets a ✓ and an accent
|
||
|
|
// background. "Automatic" sits at the top with the OS language in its
|
||
|
|
// subtitle so the user sees what it will do today.
|
||
|
|
const langRows = [];
|
||
|
|
langRows.push(`<div class="item ${isAuto ? "active" : ""}" data-pick="__auto__">
|
||
|
|
<span class="ic">⌂</span>
|
||
|
|
<div class="wrap"><div>Automatic</div><div class="sub">follow your OS (${esc(d.osLangName || "")})</div></div>
|
||
|
|
${isAuto ? '<span class="chk">✓</span>' : ""}
|
||
|
|
</div>`);
|
||
|
|
for (const L of (d.supported || [])) {
|
||
|
|
const active = curTag === L.tag.toLowerCase();
|
||
|
|
langRows.push(`<div class="item ${active ? "active" : ""}" data-pick="${esc(L.tag)}">
|
||
|
|
<span class="ic">${esc(L.code || "")}</span><span class="nm">${esc(L.label)}</span>
|
||
|
|
${active ? '<span class="chk">✓</span>' : ""}
|
||
|
|
</div>`);
|
||
|
|
}
|
||
|
|
$("langs").innerHTML = langRows.join("");
|
||
|
|
|
||
|
|
// Unsupported languages collapsible. Current pin, if it is one of these,
|
||
|
|
// is shown as active even inside the collapsed group so the state is not
|
||
|
|
// hidden. Clicking an unsupported one still sets Accept-Language.
|
||
|
|
const unsup = (d.unsupported || []);
|
||
|
|
const moreRows = unsup.map((L) => {
|
||
|
|
const active = curTag === L.tag.toLowerCase();
|
||
|
|
return `<div class="item disabled" data-pick="${esc(L.tag)}" title="Translator doesn't support ${esc(L.label)} yet. Picking it sets Accept-Language only.">
|
||
|
|
<span class="ic">${esc(L.code || "")}</span><span class="nm">${esc(L.label)}</span>
|
||
|
|
${active ? '<span class="chk">✓</span>' : '<span class="tag">soon</span>'}
|
||
|
|
</div>`;
|
||
|
|
}).join("");
|
||
|
|
$("moreList").innerHTML = moreRows;
|
||
|
|
$("moreToggle").hidden = unsup.length === 0;
|
||
|
|
|
||
|
|
// Any pinned-unsupported state auto-expands the group so the ✓ is in view.
|
||
|
|
const pinnedUnsup = unsup.some((L) => curTag === L.tag.toLowerCase());
|
||
|
|
if (pinnedUnsup) {
|
||
|
|
$("moreToggle").classList.add("open");
|
||
|
|
$("moreList").classList.add("open");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Wire clicks (fresh handlers after innerHTML).
|
||
|
|
document.querySelectorAll("[data-pick]").forEach((el) => el.onclick = () => {
|
||
|
|
if (el.classList.contains("disabled")) {
|
||
|
|
// Still allow setting Accept-Language for unsupported tags; the user
|
||
|
|
// is told "no translation" via the sub-label + tooltip.
|
||
|
|
}
|
||
|
|
window.langpicker.pick(el.dataset.pick);
|
||
|
|
});
|
||
|
|
document.querySelectorAll("[data-act]").forEach((el) => el.onclick = () => {
|
||
|
|
window.langpicker[el.dataset.act === "revert" ? "revert" : "translate"]();
|
||
|
|
});
|
||
|
|
report();
|
||
|
|
});
|
||
|
|
$("openSettings").onclick = () => window.langpicker.openSettings();
|
||
|
|
$("moreToggle").onclick = () => {
|
||
|
|
$("moreToggle").classList.toggle("open");
|
||
|
|
$("moreList").classList.toggle("open");
|
||
|
|
report();
|
||
|
|
};
|
||
|
|
</script>
|
||
|
|
</body></html>
|