The ⓘ panel leads with the map's short description, and the strategy opens on request

At the top of every "What can this map do?" panel: a "Short description" header and the map's
intro, on a pale band with the Rudder blue as its accent bar and the text in a darker shade of that
blue (5.6:1), so the eye finds it first. Below it, the strategy is a collapsed "Strategy ▾" disclosure
(a native details element, collapsed again on every open) instead of a tab; the examples follow
wherever a map has them. The tab machinery goes. New label mapInfo.shortDescription in all 11
locales.
This commit is contained in:
Doxa Maps 2026-09-30 09:26:58 +00:00
parent f065ee504e
commit c2131fe1c5
12 changed files with 41 additions and 43 deletions

View file

@ -68,9 +68,9 @@ const props = defineProps({
/**
* 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.
* the number of points is the catalogue's business, never a count kept here. The popup shows the short
* description (the lead) first, then the strategy as a collapsed disclosure, then the examples. The
* takeaway is not shown beside a strategy, which supersedes it.
*/
strategyKey: { type: String, default: '' },
note: { type: String, default: '' },
@ -130,19 +130,7 @@ function strategyPoints() {
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')
// The ARIA tabs pattern: Left/Right move between the tabs and focus follows. With two tabs either key simply
// switches, so a right-to-left reader needs no mirrored mapping.
const tabIds = () => ({ ex: 'mi-tab-ex-' + props.storageKey, st: 'mi-tab-st-' + props.storageKey, panel: 'mi-panel-' + props.storageKey })
function onTabKey(e) {
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return
e.preventDefault()
const list = e.currentTarget // null once dispatch ends, so take it now, not inside nextTick
view.value = view.value === 'examples' ? 'strategy' : 'examples'
nextTick(() => { const btn = list && list.querySelector('[aria-selected="true"]'); if (btn) btn.focus() })
}
const num = (n) => { try { return new Intl.NumberFormat(lang()).format(n) } catch { return String(n) } }
const isRtl = () => lang() === 'ar'
@ -199,7 +187,6 @@ 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()
@ -449,17 +436,24 @@ useShadowStyles(`
.mi-close:focus-visible, .mi-toggle:focus-visible, .mi-hit:focus-visible, .mi-full:focus-visible{
outline:3px solid #2563eb; outline-offset:2px; }
.mi-body{ overflow-y:auto; padding:16px 20px 20px; -webkit-overflow-scrolling:touch; }
.mi-lead{ margin:0 0 6px; font-size:16px; }
/* The short description is the first thing the eye should find: the Rudder blue (#38bdf8) as the ACCENT
(a left bar on a pale band of the same hue), and the text in a darker shade of that hue (#0369a1), since
the accent itself is only ~2.1:1 on white and would be hard to read as text. */
.mi-bluf{ margin:0 0 12px; padding:10px 12px; background:#f0f9ff; border-inline-start:4px solid #38bdf8; border-radius:6px; }
.mi-bluf-h{ margin:0 0 4px; font-size:13px; font-weight:700; color:#0c4a6e; }
.mi-lead{ margin:0; font-size:16px; font-weight:500; color:#0369a1; }
.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-strategy{ margin:14px 0 0; border:1px solid #e6e8ee; border-radius:10px; }
.mi-strategy-sum{ display:flex; align-items:center; justify-content:space-between; gap:8px; min-height:44px;
padding:0 14px; cursor:pointer; font-weight:700; color:#1b1f2a; list-style:none; }
.mi-strategy-sum::-webkit-details-marker{ display:none; }
.mi-strategy-sum:focus-visible{ outline:3px solid #2563eb; outline-offset:2px; border-radius:10px; }
.mi-chev{ transition:transform .15s; }
.mi-strategy[open] .mi-chev{ transform:rotate(180deg); }
.mi-strategy > .mi-bottom, .mi-strategy > .mi-points{ margin-inline:14px; }
.mi-strategy > :last-child{ margin-bottom:14px; }
.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; }
@ -554,6 +548,7 @@ useShadowStyles(`
styles are injected: a page-level rule cannot reach into a shadow root. */
@media (prefers-reduced-motion: reduce){
.mi-pointer, .mi .mi-pointer.mi-ctl{ animation:none; }
.mi-chev{ transition:none; }
}
`, 'map-info')
</script>
@ -594,27 +589,22 @@ useShadowStyles(`
</div>
<div class="mi-body">
<p v-if="leadKey || lead" class="mi-lead">{{ text(leadKey, lead) }}</p>
<!-- BOTTOM LINE FIRST: the map's short description, marked, in blue, so the eye lands there. -->
<section v-if="leadKey || lead" class="mi-bluf" :aria-labelledby="'mi-bluf-' + storageKey">
<h3 :id="'mi-bluf-' + storageKey" class="mi-bluf-h">{{ t('mapInfo.shortDescription') }}</h3>
<p class="mi-lead">{{ text(leadKey, lead) }}</p>
</section>
<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" @keydown="onTabKey">
<button :id="tabIds().ex" type="button" role="tab" class="mi-tab" :aria-selected="String(view === 'examples')" :aria-controls="tabIds().panel"
:tabindex="view === 'examples' ? 0 : -1" @click="view = 'examples'">{{ t('mapInfo.tabExamples') }}</button>
<button :id="tabIds().st" type="button" role="tab" class="mi-tab" :aria-selected="String(view === 'strategy')" :aria-controls="tabIds().panel"
:tabindex="view === 'strategy' ? 0 : -1" @click="view = 'strategy'">{{ t('mapInfo.tabStrategy') }}</button>
</div>
<div :id="hasTabs() ? tabIds().panel : null" :role="hasTabs() ? 'tabpanel' : null"
:aria-labelledby="hasTabs() ? (view === 'examples' ? tabIds().ex : tabIds().st) : null">
<section v-if="hasStrategy() && (!hasTabs() || view === 'strategy')" class="mi-strategy">
<h3 v-if="!hasTabs()">{{ t('mapInfo.tabStrategy') }}</h3>
<!-- The strategy, collapsed until asked for (a native disclosure: keyboard and screen readers for free).
Keyed by the open count, so every open starts collapsed. -->
<details v-if="hasStrategy()" :key="'st-' + opens" class="mi-strategy">
<summary class="mi-strategy-sum"><span>{{ t('mapInfo.tabStrategy') }}</span><span class="mi-chev" aria-hidden="true">▾</span></summary>
<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'">
</details>
<section v-for="(ex, i) in examples" v-show="!failed[i]" :key="i" class="mi-ex">
<h3>{{ titleOf(ex) }}</h3>
@ -681,9 +671,6 @@ useShadowStyles(`
</template>
</section>
</template>
</div>
<p v-if="noteKey || note" class="mi-note">{{ text(noteKey, note) }}</p>
</div>

View file

@ -1,6 +1,7 @@
{
"open": "ماذا يمكن أن تقدّم لك هذه الخريطة؟",
"title": "ماذا يمكن أن تقدّم لك هذه الخريطة؟",
"shortDescription": "وصف موجز",
"pointer": "ماذا تفعل هذه الخريطة؟",
"close": "إغلاق",
"takeaway": "ما تحصل عليه:",

View file

@ -1,6 +1,7 @@
{
"open": "Was kann diese Karte für Sie tun?",
"title": "Was kann diese Karte für Sie tun?",
"shortDescription": "Kurzbeschreibung",
"pointer": "Was kann diese Karte?",
"close": "Schließen",
"takeaway": "Was Sie mitnehmen:",

View file

@ -1,6 +1,7 @@
{
"open": "What can this map do for you?",
"title": "What can this map do for you?",
"shortDescription": "Short description",
"pointer": "What can this map do?",
"close": "Close",
"takeaway": "What you take away:",

View file

@ -1,6 +1,7 @@
{
"open": "¿Qué puede hacer este mapa por usted?",
"title": "¿Qué puede hacer este mapa por usted?",
"shortDescription": "Descripción breve",
"pointer": "¿Qué hace este mapa?",
"close": "Cerrar",
"takeaway": "Lo que se lleva:",

View file

@ -1,6 +1,7 @@
{
"open": "Que peut faire cette carte pour vous ?",
"title": "Que peut faire cette carte pour vous ?",
"shortDescription": "Description courte",
"pointer": "Que fait cette carte ?",
"close": "Fermer",
"takeaway": "Ce que vous emportez :",

View file

@ -1,6 +1,7 @@
{
"open": "यह नक्शा आपके लिए क्या कर सकता है?",
"title": "यह नक्शा आपके लिए क्या कर सकता है?",
"shortDescription": "संक्षिप्त विवरण",
"pointer": "यह नक्शा क्या करता है?",
"close": "बंद करें",
"takeaway": "आपको क्या मिलता है:",

View file

@ -1,6 +1,7 @@
{
"open": "Che cosa può fare questa mappa per te?",
"title": "Che cosa può fare questa mappa per te?",
"shortDescription": "Breve descrizione",
"pointer": "Che cosa fa questa mappa?",
"close": "Chiudi",
"takeaway": "Che cosa ottieni:",

View file

@ -1,6 +1,7 @@
{
"open": "O que este mapa pode fazer por você?",
"title": "O que este mapa pode fazer por você?",
"shortDescription": "Descrição breve",
"pointer": "O que este mapa faz?",
"close": "Fechar",
"takeaway": "O que você leva:",

View file

@ -1,6 +1,7 @@
{
"open": "Ce poate face această hartă pentru tine?",
"title": "Ce poate face această hartă pentru tine?",
"shortDescription": "Descriere scurtă",
"pointer": "Ce face această hartă?",
"close": "Închide",
"takeaway": "Ce primești:",

View file

@ -1,6 +1,7 @@
{
"open": "Чем эта карта может быть вам полезна?",
"title": "Чем эта карта может быть вам полезна?",
"shortDescription": "Краткое описание",
"pointer": "Что умеет эта карта?",
"close": "Закрыть",
"takeaway": "Что вы получаете:",

View file

@ -1,6 +1,7 @@
{
"open": "这张地图能为你做什么?",
"title": "这张地图能为你做什么?",
"shortDescription": "简要说明",
"pointer": "这张地图能做什么?",
"close": "关闭",
"takeaway": "你能带走的:",