feat(sirius-x): language picker on the right with globe icon and flags
The native select sat mid-nav next to the font pill and was easy to miss. Replace it with a custom dropdown placed just before the wallet button: globe icon, the current language's flag and native name, a menu of all languages with flags. Flags are tiny inline SVGs rather than emoji (Windows renders flag emoji as bare letters) or a flag CDN (no third-party request from the visitor's browser). Painted on window load so it lands after the deferred nav scripts; keyboard: arrows, Escape, click outside.
This commit is contained in:
parent
08a0c8d33f
commit
085824faf6
5 changed files with 118 additions and 23 deletions
|
|
@ -92,7 +92,7 @@
|
|||
<script type="importmap">
|
||||
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
|
||||
</script>
|
||||
<script src="../js/i18n.js?v=20260917a"></script>
|
||||
<script src="../js/i18n.js?v=20260917b"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@
|
|||
.verify dd{margin:2px 0 0;font-family:ui-monospace,monospace;font-size:12.5px;word-break:break-all;color:var(--ink)}
|
||||
footer{border-top:1px solid var(--line);padding:2.4rem 0 3.5rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
|
||||
</style>
|
||||
<script src="./js/i18n.js?v=20260917a"></script>
|
||||
<script src="./js/i18n.js?v=20260917b"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
|||
133
js/i18n.js
133
js/i18n.js
|
|
@ -203,50 +203,145 @@
|
|||
}
|
||||
|
||||
// ---------- the dropdown ----------
|
||||
var picker = null;
|
||||
// A custom menu rather than a <select>: a native select cannot show
|
||||
// flags, and emoji flags render as bare letters on Windows. Flags are
|
||||
// tiny inline SVGs (3:2, simplified) so no third-party image request
|
||||
// ever leaves the visitor's browser. Sits on the right of the top nav,
|
||||
// just before the wallet button.
|
||||
var FLAGS = {
|
||||
en: '<rect width="60" height="40" fill="#012169"/><path d="M0 0L60 40M60 0L0 40" stroke="#fff" stroke-width="8"/><path d="M0 0L60 40M60 0L0 40" stroke="#C8102E" stroke-width="3"/><path d="M30 0V40M0 20H60" stroke="#fff" stroke-width="10"/><path d="M30 0V40M0 20H60" stroke="#C8102E" stroke-width="6"/>',
|
||||
de: '<rect width="60" height="40" fill="#000"/><rect y="13.33" width="60" height="13.34" fill="#D00"/><rect y="26.67" width="60" height="13.33" fill="#FFCE00"/>',
|
||||
el: '<rect width="60" height="40" fill="#0D5EAF"/><rect y="4.44" width="60" height="4.45" fill="#fff"/><rect y="13.33" width="60" height="4.45" fill="#fff"/><rect y="22.22" width="60" height="4.45" fill="#fff"/><rect y="31.11" width="60" height="4.45" fill="#fff"/><rect width="22.22" height="22.22" fill="#0D5EAF"/><rect x="8.89" width="4.44" height="22.22" fill="#fff"/><rect y="8.89" width="22.22" height="4.44" fill="#fff"/>',
|
||||
ru: '<rect width="60" height="40" fill="#fff"/><rect y="13.33" width="60" height="13.34" fill="#0039A6"/><rect y="26.67" width="60" height="13.33" fill="#D52B1E"/>',
|
||||
es: '<rect width="60" height="40" fill="#AA151B"/><rect y="10" width="60" height="20" fill="#F1BF00"/>',
|
||||
fr: '<rect width="20" height="40" fill="#0055A4"/><rect x="20" width="20" height="40" fill="#fff"/><rect x="40" width="20" height="40" fill="#EF4135"/>',
|
||||
pt: '<rect width="60" height="40" fill="#F00"/><rect width="24" height="40" fill="#060"/><circle cx="24" cy="20" r="7" fill="#FFE000"/><circle cx="24" cy="20" r="3.5" fill="#F00"/>',
|
||||
};
|
||||
var GLOBE = '<svg class="lp-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3.5 3 14.5 0 18M12 3c-3 3.5-3 14.5 0 18"/></svg>';
|
||||
var CARET = '<svg class="lp-caret" viewBox="0 0 10 6" aria-hidden="true"><path d="M1 1l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>';
|
||||
function flagSvg(code) {
|
||||
return '<svg class="lp-flag" viewBox="0 0 60 40" aria-hidden="true">' + (FLAGS[code] || "") + "</svg>";
|
||||
}
|
||||
|
||||
var picker = null, pickerBtn = null, pickerMenu = null;
|
||||
function closePicker() {
|
||||
if (!picker) return;
|
||||
picker.classList.remove("open");
|
||||
pickerBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
function openPicker() {
|
||||
if (!picker) return;
|
||||
picker.classList.add("open");
|
||||
pickerBtn.setAttribute("aria-expanded", "true");
|
||||
var active = pickerMenu.querySelector('[aria-selected="true"]');
|
||||
if (active) active.focus();
|
||||
}
|
||||
function paintPicker() {
|
||||
if (picker) return;
|
||||
var css = document.createElement("style");
|
||||
css.textContent =
|
||||
".lang-picker{margin:0 6px;padding:4px 26px 4px 10px;border-radius:999px;background:rgba(255,255,255,.04);" +
|
||||
"border:1px solid rgba(255,255,255,.08);color:var(--mut,#b8c2d4);font:inherit;font-size:12px;cursor:pointer;" +
|
||||
"-webkit-appearance:none;appearance:none;background-image:url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%23b8c2d4' stroke-width='1.5'/></svg>\");" +
|
||||
"background-repeat:no-repeat;background-position:right 10px center}" +
|
||||
".lang-picker:hover{color:var(--ink,#f1f4fa);border-color:rgba(255,255,255,.18)}" +
|
||||
".lang-picker option{background:#141a24;color:#f1f4fa}" +
|
||||
".lang-picker-float{position:fixed;bottom:20px;left:20px;z-index:99;backdrop-filter:blur(10px)}";
|
||||
".lang-picker{position:relative;margin-left:auto;font-family:var(--sans,inherit)}" +
|
||||
".topnav .lang-picker + .profile-wrap,.topnav .lang-picker + a.portal{margin-left:8px}" +
|
||||
".lp-btn{display:inline-flex;align-items:center;gap:7px;padding:5px 10px 5px 9px;border-radius:999px;" +
|
||||
"background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);color:var(--mut,#b8c2d4);" +
|
||||
"font:inherit;font-size:12.5px;line-height:1;cursor:pointer;transition:border-color .12s,color .12s,background .12s}" +
|
||||
".lp-btn:hover,.lang-picker.open .lp-btn{color:var(--ink,#f1f4fa);border-color:rgba(214,255,61,.35);background:rgba(214,255,61,.08)}" +
|
||||
".lp-globe{width:15px;height:15px;opacity:.85;flex:none}" +
|
||||
".lp-flag{width:20px;height:14px;border-radius:2px;box-shadow:0 0 0 1px rgba(255,255,255,.14);flex:none}" +
|
||||
".lp-caret{width:9px;height:9px;opacity:.7;transition:transform .15s;flex:none}" +
|
||||
".lang-picker.open .lp-caret{transform:rotate(180deg)}" +
|
||||
".lp-menu{position:absolute;right:0;top:calc(100% + 8px);min-width:200px;padding:6px;border-radius:12px;" +
|
||||
"background:var(--panel,#141a24);border:1px solid rgba(255,255,255,.1);box-shadow:0 12px 32px rgba(0,0,0,.45);z-index:200;display:none}" +
|
||||
".lang-picker.open .lp-menu{display:block}" +
|
||||
".lp-opt{display:flex;align-items:center;gap:10px;width:100%;padding:8px 10px;border:0;border-radius:8px;" +
|
||||
"background:transparent;color:var(--mut,#b8c2d4);font:inherit;font-size:13.5px;text-align:left;cursor:pointer}" +
|
||||
".lp-opt:hover,.lp-opt:focus-visible{background:rgba(255,255,255,.06);color:var(--ink,#f1f4fa);outline:none}" +
|
||||
'.lp-opt[aria-selected="true"]{color:var(--acid,#d6ff3d);background:rgba(214,255,61,.08)}' +
|
||||
".lp-opt .lp-code{margin-left:auto;font-family:var(--mono,monospace);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;opacity:.55}" +
|
||||
".lang-picker-float{position:fixed;bottom:20px;right:20px;z-index:99}" +
|
||||
"@media (max-width:560px){.lp-name{display:none}}";
|
||||
document.head.appendChild(css);
|
||||
picker = document.createElement("select");
|
||||
|
||||
picker = document.createElement("div");
|
||||
picker.className = "lang-picker";
|
||||
picker.setAttribute("aria-label", "Language");
|
||||
picker.setAttribute("translate", "no");
|
||||
picker.title = "Language";
|
||||
pickerBtn = document.createElement("button");
|
||||
pickerBtn.type = "button";
|
||||
pickerBtn.className = "lp-btn";
|
||||
pickerBtn.setAttribute("aria-haspopup", "listbox");
|
||||
pickerBtn.setAttribute("aria-expanded", "false");
|
||||
pickerBtn.setAttribute("aria-label", "Language");
|
||||
pickerBtn.title = "Language";
|
||||
pickerMenu = document.createElement("div");
|
||||
pickerMenu.className = "lp-menu";
|
||||
pickerMenu.setAttribute("role", "listbox");
|
||||
pickerMenu.setAttribute("aria-label", "Language");
|
||||
var html = "";
|
||||
for (var code in LANGS) html += '<option value="' + code + '">' + LANGS[code] + "</option>";
|
||||
picker.innerHTML = html;
|
||||
picker.addEventListener("change", function () { setLang(picker.value); });
|
||||
for (var code in LANGS) {
|
||||
html += '<button type="button" class="lp-opt" role="option" data-lang="' + code + '" aria-selected="false" tabindex="-1">' +
|
||||
flagSvg(code) + "<span>" + LANGS[code] + '</span><span class="lp-code">' + code + "</span></button>";
|
||||
}
|
||||
pickerMenu.innerHTML = html;
|
||||
picker.appendChild(pickerBtn);
|
||||
picker.appendChild(pickerMenu);
|
||||
|
||||
pickerBtn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
if (picker.classList.contains("open")) closePicker(); else openPicker();
|
||||
});
|
||||
pickerMenu.addEventListener("click", function (e) {
|
||||
var opt = e.target.closest ? e.target.closest("[data-lang]") : null;
|
||||
if (!opt) return;
|
||||
e.stopPropagation();
|
||||
closePicker();
|
||||
pickerBtn.focus();
|
||||
setLang(opt.getAttribute("data-lang"));
|
||||
});
|
||||
picker.addEventListener("keydown", function (e) {
|
||||
var opts = pickerMenu.querySelectorAll("[data-lang]");
|
||||
var i = Array.prototype.indexOf.call(opts, document.activeElement);
|
||||
if (e.key === "Escape") { closePicker(); pickerBtn.focus(); }
|
||||
else if (e.key === "ArrowDown") { e.preventDefault(); if (!picker.classList.contains("open")) openPicker(); else opts[Math.min(i + 1, opts.length - 1)].focus(); }
|
||||
else if (e.key === "ArrowUp") { e.preventDefault(); if (i > 0) opts[i - 1].focus(); }
|
||||
});
|
||||
document.addEventListener("click", closePicker);
|
||||
|
||||
var nav = document.querySelector(".topnav");
|
||||
if (nav) {
|
||||
var portalLink = nav.querySelector("a.portal");
|
||||
var fontPicker = nav.querySelector(".font-picker");
|
||||
nav.insertBefore(picker, fontPicker || portalLink || null);
|
||||
// Right side: directly before the wallet button (profile-wrap once
|
||||
// profile-menu.js has wrapped the portal link, the bare link before
|
||||
// that), after everything else.
|
||||
var wallet = nav.querySelector(".profile-wrap") || nav.querySelector("a.portal");
|
||||
if (wallet) nav.insertBefore(picker, wallet); else nav.appendChild(picker);
|
||||
} else {
|
||||
picker.classList.add("lang-picker-float");
|
||||
document.body.appendChild(picker);
|
||||
}
|
||||
refreshPicker();
|
||||
}
|
||||
function refreshPicker() { if (picker) picker.value = lang; }
|
||||
function refreshPicker() {
|
||||
if (!picker) return;
|
||||
pickerBtn.innerHTML = GLOBE + flagSvg(lang) + '<span class="lp-name">' + (LANGS[lang] || lang) + "</span>" + CARET;
|
||||
var opts = pickerMenu.querySelectorAll("[data-lang]");
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
opts[i].setAttribute("aria-selected", opts[i].getAttribute("data-lang") === lang ? "true" : "false");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- boot ----------
|
||||
lang = detect();
|
||||
var ready = lang === "en" ? Promise.resolve(null) : load(lang).then(function (d) { dict = d; return d; }).catch(function () { lang = "en"; return null; });
|
||||
function boot() {
|
||||
observe();
|
||||
ready.then(function () { applyAll(); paintPicker(); });
|
||||
ready.then(function () { applyAll(); });
|
||||
}
|
||||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
|
||||
else boot();
|
||||
// The picker is painted on `load`, after the deferred nav scripts
|
||||
// (theme.js font pill, profile-menu.js wallet button) have laid out the
|
||||
// top nav, so it lands in a stable spot on the right.
|
||||
if (document.readyState === "complete") paintPicker();
|
||||
else window.addEventListener("load", paintPicker);
|
||||
|
||||
window.siriusI18n = {
|
||||
get lang() { return lang; },
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@
|
|||
.name-row{grid-template-columns:1fr auto}
|
||||
}
|
||||
</style>
|
||||
<script src="./js/i18n.js?v=20260917a"></script>
|
||||
<script src="./js/i18n.js?v=20260917b"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
|||
2
tld.html
2
tld.html
|
|
@ -115,7 +115,7 @@
|
|||
<script type="importmap">
|
||||
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
|
||||
</script>
|
||||
<script src="./js/i18n.js?v=20260917a"></script>
|
||||
<script src="./js/i18n.js?v=20260917b"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue