The globe-chip menu was a native Electron menu — square corners, system font, no theming beyond the OS's own context-menu paint. Replaces it with a floating overlay WebContentsView (lang-picker.html + preload), following the same pattern the engine picker and the popover already use: rounded 12px surface, acid-tint accents on the current pin, soft shadow, dark + light scheme, flush under the chip's bottom-right. The content is organised around the user's intent — translate first, pick a language second. The "Translate this page" row sits at the top when it is actionable (web tab + supported source + supported target), with the detected source and the target under the label so the user can tell what the backend will do before they click. Once a page is translated, that row flips to "Show original (<source>)". Below the action strip is Automatic + the six supported languages, each with a two-letter code chip in the left slot and a ✓ on the current pin. Unsupported languages are hidden by default inside a collapsible "More languages (translator coming later)" group — click to expand, click to collapse; a pinned-unsupported auto-expands so its ✓ stays visible. Plumbing matches engine-picker: deferred-load WebContentsView, closeOnClickAway, setBounds anchored under the chip, picker renderer reports its own content height after each render so the overlay contracts and expands with the "More languages" toggle. State pushes come from emitTranslateState (so the Translate / Show-original row updates when a page finishes auto-translating with the picker open) and from broadcastSettings (so a Settings-side language change re-paints the ✓). Verified end-to-end: picker loads, shows Automatic + 6 supported in the default list and 18 greyed in the "More" group, repaints to "⟲ Show original (Spanish)" after an auto-translate completes.
157 lines
7.6 KiB
HTML
157 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>
|