diff --git a/internal/check-i18n-parity.mjs b/internal/check-i18n-parity.mjs index 306ca4e19..32f6e2493 100644 --- a/internal/check-i18n-parity.mjs +++ b/internal/check-i18n-parity.mjs @@ -68,9 +68,9 @@ const areas = readdirSync(join(ROOT, 'en')).filter((f) => f.endsWith('.json')) // translations, add the locale here with its count and lower it as they arrive. // Never raise a number; never add a locale that is already complete. const DEBT = { - // 2026-10-03, the Share menu's 10 new share.json keys (shareable pages P3), English only for now: the Driver - // parked localization ("just English for now"). tStatic falls back to the English text, so these locales show - // English until the translations arrive; lower each to 0 then, and empty the map again. + // 2026-10-03, the Share menu's 10 new share.json keys (shareable pages P3): localization is English-only for now. + // tStatic falls back to the English text, so these locales show English until the translations arrive; + // lower each to 0 then, and empty the map again. ar: 10, de: 10, es: 10, fr: 10, hi: 10, it: 10, pt: 10, ro: 10, ru: 10, zh: 10, } diff --git a/internal/check-map-menu-anchor.mjs b/internal/check-map-menu-anchor.mjs index 8424a5a84..0fb0e04e4 100644 --- a/internal/check-map-menu-anchor.mjs +++ b/internal/check-map-menu-anchor.mjs @@ -46,7 +46,8 @@ if (!m) { if (fn) { const body = code.match(new RegExp('function\\s+' + fn[1] + '\\s*\\(\\)\\s*\\{([\\s\\S]*?)\\n\\}')) // every return that resolves a named area must carry its structural fallback on the SAME expression - const rets = body ? [...body[1].matchAll(/return\s+([^\n]*closest\([^\n]*)/g)].map((r) => r[1]) : [] + // a same-line comment is not a fallback: strip `//…` before testing each return + const rets = body ? [...body[1].matchAll(/return\s+([^\n]*closest\([^\n]*)/g)].map((r) => r[1].replace(/\s*\/\/.*$/, '')) : [] expr = rets.length && rets.every((r) => /offsetParent|parentElement|document\.body/.test(r)) ? rets.join(' ;; ') : (rets.find((r) => !/offsetParent|parentElement|document\.body/.test(r)) || '') if (!body) fails.push(`the area resolution calls ${fn[1]}(), and this gate cannot find that function to read it.`) else if (!rets.length) fails.push(`${fn[1]}() returns no named-area resolution, so this gate would pass blind.`) diff --git a/library/components/map-controls/MapControlMenu.vue b/library/components/map-controls/MapControlMenu.vue index 813857ef4..c04a04961 100644 --- a/library/components/map-controls/MapControlMenu.vue +++ b/library/components/map-controls/MapControlMenu.vue @@ -129,7 +129,7 @@ function place() { // ★ THE BLOCK THAT CONTAINS IT IS MEASURED, NOT ASSUMED. The menu is position:absolute, so its coordinates are // relative to its containing block, which the HOST's markup decides: an unpositioned wrapper hands it to the // header above. Measuring against one element and positioning against another is what sent the header menu far -// to the left (upg100, 2026-10-03). So the target point is computed in viewport coordinates and converted into +// to the left (the UPG-100 list page, 2026-10-03). So the target point is computed in viewport coordinates and converted into // the containing block's own (its padding edge, scrolled), whatever element that turns out to be. function placeBeside(tb) { const vw = document.documentElement.clientWidth || window.innerWidth || 1024 diff --git a/library/components/map-controls/ShareButton.vue b/library/components/map-controls/ShareButton.vue index 063b20ffe..c5bb319bb 100644 --- a/library/components/map-controls/ShareButton.vue +++ b/library/components/map-controls/ShareButton.vue @@ -411,8 +411,17 @@ function pageOwner() { } return false } +// A leaf page reads a token from ?tk= and leaves it in its address, so the page link drops it: a reader must +// never send a credential by pressing Share. The URL is rewritten only when tk is there, so every other link is +// the address exactly as it stands. function pageLink() { - try { if (pageOwner() && window.location.href) return window.location.href } catch { /* no window */ } + try { + if (pageOwner() && window.location.href) { + const u = new URL(window.location.href) + if (u.searchParams.has('tk')) { u.searchParams.delete('tk'); return u.href } + return window.location.href + } + } catch { /* no window */ } return shareUrl.value } @@ -518,9 +527,9 @@ const hasOwnAnchor = !providedToolbar ★ THE ANCHOR IS THE BUTTON, AND NOTHING IS DECIDED AT RENDER TIME (shareable pages P3). This used to tag `el.offsetParent || el.parentElement` as the menu's area DURING RENDER (a function ref, so it ran before MapControlMenu latched its area). Before layout offsetParent is null, so the tag fell on the button's - immediate parent: in a header with a narrow, unpositioned wrapper (upg100 measured .hdr-btns) the menu + immediate parent: in a header with a narrow, unpositioned wrapper (measured on the UPG-100 list page: .hdr-btns) the menu measured against that wrapper, clamped to its 200px floor, and was positioned against the header above - it: far to the left, and narrow (the Driver's report, 2026-10-03). Now the button only names itself; + it: far to the left, and narrow (reported 2026-10-03). Now the button only names itself; MapControlMenu resolves where the menu lives and where it goes when it OPENS, with layout known. */ function bindAnchor(el) {