The ⓘ panel shows each map's strategy: an intro, then the strategy, with Examples | Strategy tabs where a map has examples

MapInfo gains a `strategyKey` prop: a catalogue key prefix whose .bottom is the bottom line and .p1, .p2 … the
points, read in order until the first missing one (the catalogue decides how many). The popup shows the intro
(the lead), then:
- a map with examples AND a strategy gets two tabs, Examples first, then Strategy (44px targets, reset to
  Examples on every open);
- a map with only a strategy goes straight to it, under a Strategy heading;
- a map with neither is unchanged.
The takeaway is not shown beside a strategy, which supersedes it. Labels come from mapInfo.tabExamples and
mapInfo.tabStrategy. Prayer Progress's own info (for embeds) names its strategy key too.
This commit is contained in:
Doxa Maps 2026-09-30 08:04:11 +00:00
parent 63a64d1bf9
commit 9b39577830
2 changed files with 57 additions and 1 deletions

View file

@ -65,6 +65,14 @@ const props = defineProps({
takeaway: { type: String, default: '' },
/** A caveat under the examples (e.g. which countries are not supported yet). Key or string. */
noteKey: { type: String, default: '' },
/**
* The map's STRATEGY, as a catalogue key PREFIX (e.g. 'mapInfo.tab.upg-100.strategy'): <prefix>.bottom is
* the bottom line, <prefix>.p1, .p2 … the points, read in order until the first one that is missing. So
* the number of points is the catalogue's business, never a count kept here. With a strategy the popup
* shows the intro (the lead), then the strategy; a map that also has examples gets two tabs, EXAMPLES
* first. The takeaway is not shown beside a strategy, which supersedes it.
*/
strategyKey: { type: String, default: '' },
note: { type: String, default: '' },
/** The artefacts, in display order. See the header for the four kinds. */
examples: { type: Array, default: () => [] },
@ -106,6 +114,25 @@ function lang() {
}
const t = useOptionalT('', { fallback: (q) => tStatic(q, lang()) })
const text = (key, str) => (key ? t(key) : str)
// The strategy, read from the catalogue in the reader's language (tStatic falls back to English per key).
// lang() reads `opens`, so the lines are re-read on every open. A point is a non-empty string; the first
// missing one ends the list.
const MAX_POINTS = 8
const strategyBottom = () => (props.strategyKey ? tStatic(props.strategyKey + '.bottom', lang()) || '' : '')
function strategyPoints() {
const out = []
if (!props.strategyKey) return out
for (let i = 1; i <= MAX_POINTS; i++) {
const v = tStatic(props.strategyKey + '.p' + i, lang())
if (!v) break
out.push(v)
}
return out
}
const hasStrategy = () => !!strategyBottom()
// Two tabs only where there is something on both sides: examples AND a strategy.
const hasTabs = () => hasStrategy() && (props.examples || []).length > 0
const view = ref('examples')
const num = (n) => { try { return new Intl.NumberFormat(lang()).format(n) } catch { return String(n) } }
const isRtl = () => lang() === 'ar'
@ -162,6 +189,7 @@ const failed = reactive({})
async function show() {
opens.value++
open.value = true
view.value = 'examples'
// A page-load pointer is done for this load once the popup has been opened (in memory only).
if (pageLoad()) pointerDoneThisLoad.add(props.storageKey)
await nextTick()
@ -415,6 +443,16 @@ useShadowStyles(`
.mi-takeaway{ margin:0 0 14px; color:#3b4256; }
.mi-takeaway b{ color:#1b1f2a; }
.mi-ex{ margin:18px 0 0; }
.mi-tabs{ display:flex; gap:4px; margin:14px 0 0; border-bottom:1px solid #e6e8ee; }
.mi-tab{ min-height:44px; padding:0 16px; border:none; background:none; color:#3b4256; font:inherit; font-weight:600;
cursor:pointer; border-bottom:3px solid transparent; margin-bottom:-1px; }
.mi-tab[aria-selected="true"]{ color:#1b1f2a; border-bottom-color:#3b463d; }
.mi-tab:focus-visible{ outline:3px solid #2563eb; outline-offset:-3px; }
.mi-strategy{ margin:16px 0 0; }
.mi-strategy h3{ margin:0 0 6px; }
.mi-bottom{ margin:0 0 8px; font-weight:600; color:#1b1f2a; }
.mi-points{ margin:0; padding-inline-start:20px; }
.mi-points li{ margin:0 0 6px; }
.mi-ex h3{ margin:0 0 8px; font-size:15px; text-transform:uppercase; letter-spacing:.04em; color:#556; }
.mi-img{ display:block; width:100%; height:auto; border-radius:8px; background:#f4f5f8;
box-shadow:0 1px 4px rgba(0,0,0,.15); }
@ -547,7 +585,22 @@ useShadowStyles(`
<div class="mi-body">
<p v-if="leadKey || lead" class="mi-lead">{{ text(leadKey, lead) }}</p>
<p v-if="takeawayKey || takeaway" class="mi-takeaway"><b>{{ t('mapInfo.takeaway') }}</b> {{ text(takeawayKey, takeaway) }}</p>
<p v-if="(takeawayKey || takeaway) && !hasStrategy()" class="mi-takeaway"><b>{{ t('mapInfo.takeaway') }}</b> {{ text(takeawayKey, takeaway) }}</p>
<div v-if="hasTabs()" class="mi-tabs" role="tablist">
<button type="button" role="tab" class="mi-tab" :aria-selected="String(view === 'examples')" @click="view = 'examples'">{{ t('mapInfo.tabExamples') }}</button>
<button type="button" role="tab" class="mi-tab" :aria-selected="String(view === 'strategy')" @click="view = 'strategy'">{{ t('mapInfo.tabStrategy') }}</button>
</div>
<section v-if="hasStrategy() && (!hasTabs() || view === 'strategy')" class="mi-strategy">
<h3 v-if="!hasTabs()">{{ t('mapInfo.tabStrategy') }}</h3>
<p class="mi-bottom">{{ strategyBottom() }}</p>
<ul v-if="strategyPoints().length" class="mi-points">
<li v-for="(p, k) in strategyPoints()" :key="k">{{ p }}</li>
</ul>
</section>
<template v-if="!hasTabs() || view === 'examples'">
<section v-for="(ex, i) in examples" v-show="!failed[i]" :key="i" class="mi-ex">
<h3>{{ titleOf(ex) }}</h3>
@ -614,6 +667,8 @@ useShadowStyles(`
</template>
</section>
</template>
<p v-if="noteKey || note" class="mi-note">{{ text(noteKey, note) }}</p>
</div>

View file

@ -13,6 +13,7 @@ const PRAYER = {
// The approximate-location caveat, kept here where a reader who asks will always find it, whether or not
// the map shows its own pin note. The same catalogue string that note uses.
noteKey: 'pins.approximate',
strategyKey: 'mapInfo.tab.prayer-map.strategy',
examples: [],
}