From cbe9158d60680cfe798839ee286495d274f2c186 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Tue, 29 Sep 2026 00:07:45 +0000 Subject: [PATCH] UPG Neighbor: Arabic mirrors the whole tool, and How it works answers the keyboard The panels, legend, map notice, list text and arrows use the reading direction (in Arabic the panel sits on the right and the map frames its selection in the part you can see); the P-level counts use the same number format as every other count. How it works takes focus when it opens, keeps it, closes on Escape and gives focus back to its button. --- plugin-designs/upg-neighbor/src/campaigns.vue | 17 +++++++----- plugin-designs/upg-neighbor/src/mobilize.vue | 27 +++++++++++-------- plugin-designs/upg-neighbor/upg-neighbor.vue | 23 +++++++++++----- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/plugin-designs/upg-neighbor/src/campaigns.vue b/plugin-designs/upg-neighbor/src/campaigns.vue index 8bc1e0d3e..280785341 100644 --- a/plugin-designs/upg-neighbor/src/campaigns.vue +++ b/plugin-designs/upg-neighbor/src/campaigns.vue @@ -112,7 +112,9 @@ function frame() { const lngs = pts.map((g) => g.lng), lats = pts.map((g) => g.lat) const w = mapContainer.value?.clientWidth || 0, h = mapContainer.value?.clientHeight || 0 // The panel covers the left 440px on wide screens, the bottom 55% on a phone: frame what you can see. - const padding = w > 760 ? { top: 60, bottom: 60, left: 440, right: 60 } : { top: 40, bottom: Math.round(h * 0.55) + 20, left: 30, right: 30 } + // In a right-to-left locale the panel sits on the right, so the wide padding moves with it. + const rtl = !!mapContainer.value && getComputedStyle(mapContainer.value).direction === 'rtl' + const padding = w > 760 ? { top: 60, bottom: 60, left: rtl ? 60 : 440, right: rtl ? 440 : 60 } : { top: 40, bottom: Math.round(h * 0.55) + 20, left: 30, right: 30 } try { m.stop() m.fitBounds([[Math.min(...lngs), Math.min(...lats)], [Math.max(...lngs), Math.max(...lats)]], { padding, maxZoom: 5.5, duration: 900 }) @@ -161,7 +163,7 @@ onBeforeUnmount(() => destroy()) useShadowStyles(` .cp-root { position:absolute; inset:0; background:#f6f7f9; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#1f2937; } .cp-map { position:absolute; inset:0; } - .cp-panel { position:absolute; top:10px; left:10px; bottom:10px; width:420px; z-index:20; display:flex; flex-direction:column; + .cp-panel { position:absolute; top:10px; inset-inline-start:10px; bottom:10px; width:420px; z-index:20; display:flex; flex-direction:column; background:#fff; border:1px solid #d9dee6; border-radius:12px; box-shadow:0 6px 20px rgba(0,20,60,.12); overflow:hidden; } .cp-head { padding:12px 14px 10px; border-bottom:1px solid #eef0f4; } .cp-head p { margin:0 0 8px; font-size:12.5px; line-height:1.4; color:#4b5563; } @@ -171,7 +173,7 @@ useShadowStyles(` .cp-btn { border:1px solid ${NAVY}; background:${NAVY}; color:#fff; border-radius:8px; padding:7px 10px; font:600 12px/1 inherit; cursor:pointer; } .cp-btn.ghost { background:#fff; color:${NAVY}; } .cp-body { flex:1 1 0; overflow-y:auto; } - .cp-row { display:block; width:100%; text-align:left; border:0; border-bottom:1px solid #f1f3f6; background:#fff; padding:10px 14px; cursor:pointer; font:inherit; color:inherit; } + .cp-row { display:block; width:100%; text-align:start; border:0; border-bottom:1px solid #f1f3f6; background:#fff; padding:10px 14px; cursor:pointer; font:inherit; color:inherit; } .cp-row:hover, .cp-row:focus-visible { background:#f5f8ff; } .cp-row b { font-size:14px; } .cp-bar { display:flex; gap:10px; margin-top:4px; font-size:12px; color:#4b5563; flex-wrap:wrap; } @@ -180,6 +182,7 @@ useShadowStyles(` .cp-where { font-size:11.5px; color:#6b7280; margin-top:3px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .cp-brief { padding:12px 14px 16px; } .cp-back { border:0; background:none; color:${NAVY}; font:600 12px/1 inherit; cursor:pointer; padding:0 0 8px; } + .cp-back::before { content:'← '; } [dir="rtl"] .cp-back::before { content:'→ '; } .cp-title { font:700 18px/1.2 inherit; margin:0 0 4px; } .cp-big { display:flex; gap:10px; margin:10px 0; } .cp-big div { flex:1; border-radius:10px; padding:10px; font-size:12px; line-height:1.3; } @@ -189,11 +192,11 @@ useShadowStyles(` .cp-res span.yes { border-color:#9fd3b0; color:#0b5e33; background:#eaf7ef; } .cp-brief h3 { font:700 13px/1.2 inherit; margin:14px 0 6px; } .cp-line { display:flex; justify-content:space-between; gap:8px; font-size:12.5px; padding:5px 0; border-bottom:1px solid #f1f3f6; } - .cp-line button { border:0; background:none; padding:0; font:inherit; color:${NAVY}; cursor:pointer; text-align:left; } + .cp-line button { border:0; background:none; padding:0; font:inherit; color:${NAVY}; cursor:pointer; text-align:start; } .cp-note { padding:14px; color:#6b7280; font-size:13px; line-height:1.45; } .cp-hover { position:absolute; z-index:25; pointer-events:none; background:rgba(0,27,68,.92); color:#fff; font-size:12px; line-height:1.3; padding:5px 8px; border-radius:6px; max-width:260px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } - .cp-maperr { position:absolute; right:12px; top:12px; z-index:15; background:#fff; border:1px solid #d9dee6; border-radius:8px; padding:8px 12px; font-size:13px; color:#6b7280; } - @media (max-width:760px) { .cp-panel { top:auto; left:6px; right:6px; bottom:6px; width:auto; height:55%; } } + .cp-maperr { position:absolute; inset-inline-end:12px; top:12px; z-index:15; background:#fff; border:1px solid #d9dee6; border-radius:8px; padding:8px 12px; font-size:13px; color:#6b7280; } + @media (max-width:760px) { .cp-panel { top:auto; inset-inline:6px; bottom:6px; width:auto; height:55%; } } `, 'upgn-campaigns') @@ -236,7 +239,7 @@ useShadowStyles(`
- +

{{ active.langName }}

{{ fmtNum(active.believers) }}{{ $t('neighbor.camp.bigBelievers', { g: active.reached.length, c: active.reachedCountries.length }) }}
diff --git a/plugin-designs/upg-neighbor/src/mobilize.vue b/plugin-designs/upg-neighbor/src/mobilize.vue index 0264cdaec..0e9f59286 100644 --- a/plugin-designs/upg-neighbor/src/mobilize.vue +++ b/plugin-designs/upg-neighbor/src/mobilize.vue @@ -224,9 +224,11 @@ function frame() { } // The panel covers part of the map: the left 400px on wide screens, the bottom 55% on a phone. // Frame the selection in the part you can see. +// In a right-to-left locale the panel sits on the right, so the wide padding moves with it. function framePadding() { const el = mapContainer.value, w = el?.clientWidth || 0, h = el?.clientHeight || 0 - return w > 760 ? { top: 60, bottom: 60, left: 400, right: 60 } : { top: 40, bottom: Math.round(h * 0.55) + 20, left: 30, right: 30 } + const rtl = !!el && getComputedStyle(el).direction === 'rtl' + return w > 760 ? { top: 60, bottom: 60, left: rtl ? 60 : 400, right: rtl ? 400 : 60 } : { top: 40, bottom: Math.round(h * 0.55) + 20, left: 30, right: 30 } } watch(() => [nbState.pgid, pFilter.value, data.value, eLevel.value], () => { repaint(); frame() }) @@ -272,7 +274,7 @@ onBeforeUnmount(() => destroy()) useShadowStyles(` .mb-root { position:absolute; inset:0; display:flex; background:#f6f7f9; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; color:#1f2937; } .mb-map { position:absolute; inset:0; } - .mb-panel { position:absolute; top:10px; left:10px; bottom:10px; width:380px; z-index:20; display:flex; flex-direction:column; + .mb-panel { position:absolute; top:10px; inset-inline-start:10px; bottom:10px; width:380px; z-index:20; display:flex; flex-direction:column; background:#fff; border:1px solid #d9dee6; border-radius:12px; box-shadow:0 6px 20px rgba(0,20,60,.12); overflow:hidden; } .mb-head { padding:12px 14px 10px; border-bottom:1px solid #eef0f4; } .mb-search { width:100%; box-sizing:border-box; border:1px solid #c9d1dd; border-radius:8px; padding:9px 11px; font:14px/1.2 inherit; } @@ -281,7 +283,7 @@ useShadowStyles(` .mb-chip { border:1px solid #c9d1dd; background:#fff; color:#374151; border-radius:999px; padding:4px 10px; font:600 12px/1 inherit; cursor:pointer; } .mb-chip.on { background:${NAVY}; border-color:${NAVY}; color:#fff; } .mb-body { flex:1 1 0; overflow-y:auto; } - .mb-row { display:flex; align-items:center; gap:10px; width:100%; text-align:left; border:0; border-bottom:1px solid #f1f3f6; background:#fff; padding:9px 14px; cursor:pointer; font:inherit; color:inherit; } + .mb-row { display:flex; align-items:center; gap:10px; width:100%; text-align:start; border:0; border-bottom:1px solid #f1f3f6; background:#fff; padding:9px 14px; cursor:pointer; font:inherit; color:inherit; } .mb-row:hover, .mb-row:focus-visible { background:#f5f8ff; } .mb-row-main { flex:1 1 auto; min-width:0; } .mb-row-name { font:600 13px/1.25 inherit; } @@ -289,6 +291,9 @@ useShadowStyles(` .mb-pbadge { flex:0 0 auto; font:700 11px/1 ui-monospace,monospace; border-radius:6px; padding:4px 6px; color:#fff; } .mb-brief { padding:12px 14px 16px; } .mb-back { border:0; background:none; color:${NAVY}; font:600 12px/1 inherit; cursor:pointer; padding:0 0 8px; } + /* Arrows point the way the page reads (a plain "←" does not mirror in Arabic). */ + .mb-back::before { content:'← '; } [dir="rtl"] .mb-back::before { content:'→ '; } + .mb-go::after { content:' →'; } [dir="rtl"] .mb-go::after { content:' ←'; } .mb-title { font:700 17px/1.2 inherit; margin:0; } .mb-sub { color:#4b5563; font-size:13px; margin:4px 0 10px; } .mb-p { border-radius:10px; padding:10px 12px; margin-bottom:10px; color:#0b2e1a; font-size:13px; line-height:1.4; } @@ -313,15 +318,15 @@ useShadowStyles(` .mb-card-main:focus-visible { outline:2px solid ${NAVY}; outline-offset:2px; border-radius:4px; } .mb-pivot { margin-top:6px; min-height:28px; border:1px solid ${NAVY}; background:#fff; color:${NAVY}; border-radius:6px; padding:4px 10px; font:600 12px/1 inherit; cursor:pointer; } .mb-pivot:hover, .mb-pivot:focus-visible { background:${NAVY}; color:#fff; } - .mb-same { font:700 10px/1 inherit; color:#7a3e00; background:#fdeedd; border-radius:999px; padding:3px 6px; margin-left:4px; } + .mb-same { font:700 10px/1 inherit; color:#7a3e00; background:#fdeedd; border-radius:999px; padding:3px 6px; margin-inline-start:4px; } .mb-note { padding:14px; color:#6b7280; font-size:13px; line-height:1.45; } - .mb-maperr { position:absolute; right:12px; top:12px; z-index:15; background:#fff; border:1px solid #d9dee6; border-radius:8px; padding:8px 12px; font-size:13px; color:#6b7280; } + .mb-maperr { position:absolute; inset-inline-end:12px; top:12px; z-index:15; background:#fff; border:1px solid #d9dee6; border-radius:8px; padding:8px 12px; font-size:13px; color:#6b7280; } .mb-hover { position:absolute; z-index:25; pointer-events:none; background:rgba(0,27,68,.92); color:#fff; font-size:12px; line-height:1.3; padding:5px 8px; border-radius:6px; max-width:260px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } - .mb-legend { position:absolute; right:12px; bottom:28px; z-index:15; background:rgba(255,255,255,.95); border:1px solid #d9dee6; border-radius:10px; padding:8px 10px; font-size:12px; } + .mb-legend { position:absolute; inset-inline-end:12px; bottom:28px; z-index:15; background:rgba(255,255,255,.95); border:1px solid #d9dee6; border-radius:10px; padding:8px 10px; font-size:12px; } .mb-legend div { display:flex; align-items:center; gap:7px; padding:2px 0; } @media (max-width:760px) { - .mb-panel { top:auto; left:6px; right:6px; bottom:6px; width:auto; height:55%; } - .mb-legend { top:8px; bottom:auto; right:8px; } + .mb-panel { top:auto; inset-inline:6px; bottom:6px; width:auto; height:55%; } + .mb-legend { top:8px; bottom:auto; inset-inline-end:8px; } } `, 'upgn-mobilize') @@ -345,7 +350,7 @@ useShadowStyles(`
+ @click="pFilter = p">P-{{ p }} · {{ (pCounts[p] || 0).toLocaleString('en-US') }}
@@ -367,7 +372,7 @@ useShadowStyles(`
- +

{{ selected.name }}

{{ selected.country }} · {{ selected.langName }} · {{ $t('neighbor.mob.people', { n: fmtNum(selected.pop) }) }} · {{ selected.religion }}
@@ -412,7 +417,7 @@ useShadowStyles(` P-{{ n.g.p || '–' }} · {{ $t('neighbor.mob.cardStatsU', { p: fmtNum(n.g.pop), km: n.km.toLocaleString('en-US') }) }} {{ $t('neighbor.mob.cardStats', { b: fmtNum(n.g.believers), km: n.km.toLocaleString('en-US') }) }} - +
diff --git a/plugin-designs/upg-neighbor/upg-neighbor.vue b/plugin-designs/upg-neighbor/upg-neighbor.vue index ce0adb567..642a717a1 100644 --- a/plugin-designs/upg-neighbor/upg-neighbor.vue +++ b/plugin-designs/upg-neighbor/upg-neighbor.vue @@ -19,7 +19,7 @@ * 2026-09-27: they ran on a 40-country snapshot and answered one question five ways. Their view names * still open the nearest new view, so existing embeds keep working. */ -import { inject, reactive, computed, provide } from 'vue' +import { inject, reactive, computed, provide, ref, watch, nextTick } from 'vue' import { useShadowStyles } from '@map/composables/useShadowStyles.js' import Mobilize from './src/mobilize.vue' import Campaigns from './src/campaigns.vue' @@ -39,16 +39,25 @@ const initial = VIEWS.includes(asked) ? asked : (LEGACY[asked] || (cfg.value.lan const nbState = reactive({ view: initial, pgid: cfg.value.pgid || null, lang: cfg.value.lang || null, about: false }) provide('nbState', nbState) +// "How it works" is a real dialog for the keyboard: focus moves to its close button when it opens, Tab +// stays there (its only control), Escape closes it, and focus returns to the button that opened it. +const aboutBtn = ref(null), aboutClose = ref(null) +watch(() => nbState.about, (open) => nextTick(() => (open ? aboutClose.value : aboutBtn.value)?.focus?.())) +function onAboutKey(e) { + if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); nbState.about = false } + else if (e.key === 'Tab') { e.preventDefault(); aboutClose.value?.focus?.() } +} + useShadowStyles(` .dun-root { position:absolute; inset:0; display:flex; flex-direction:column; overflow:hidden; background:#fff; isolation:isolate; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; } .dun-bar { flex:0 0 auto; display:flex; align-items:center; gap:10px; padding:8px 12px; background:${NAVY}; z-index:50; flex-wrap:wrap; } - .dun-brand { font:700 13px/1 inherit; color:#fff; white-space:nowrap; margin-right:4px; } + .dun-brand { font:700 13px/1 inherit; color:#fff; white-space:nowrap; margin-inline-end:4px; } .dun-seg { display:inline-flex; background:rgba(255,255,255,.08); border-radius:999px; padding:3px; gap:2px; } .dun-seg button { border:0; background:transparent; color:#c7d3ea; cursor:pointer; font:600 13px/1 inherit; padding:7px 14px; border-radius:999px; } .dun-seg button.on { background:#fff; color:${NAVY}; } .dun-seg button:focus-visible, .dun-about:focus-visible { outline:2px solid #fff; outline-offset:2px; } - .dun-about { margin-left:auto; border:1px solid rgba(255,255,255,.35); background:transparent; color:#fff; border-radius:999px; padding:6px 12px; font:600 12px/1 inherit; cursor:pointer; } + .dun-about { margin-inline-start:auto; border:1px solid rgba(255,255,255,.35); background:transparent; color:#fff; border-radius:999px; padding:6px 12px; font:600 12px/1 inherit; cursor:pointer; } .dun-stage { flex:1 1 0; position:relative; min-height:0; } .dun-modal { position:absolute; inset:0; z-index:60; background:rgba(0,20,50,.45); display:flex; align-items:center; justify-content:center; padding:16px; } .dun-card { background:#fff; border-radius:12px; max-width:620px; max-height:100%; overflow-y:auto; padding:18px 20px; font-size:14px; line-height:1.5; color:#1f2937; } @@ -56,7 +65,7 @@ useShadowStyles(` .dun-card h3 { margin:14px 0 4px; font-size:14px; } .dun-card p { margin:0 0 8px; } .dun-card .src { font-size:12px; color:#6b7280; } - .dun-close { float:right; border:0; background:none; font-size:20px; line-height:1; cursor:pointer; color:#6b7280; } + .dun-close { float:inline-end; border:0; background:none; font-size:20px; line-height:1; cursor:pointer; color:#6b7280; } @media (max-width:640px){ .dun-brand{ display:none; } .dun-seg button { padding:7px 10px; } } `, 'upg-neighbor') @@ -69,16 +78,16 @@ useShadowStyles(`
- +
-