An Experimental map has a "?" that says what it is for and how to use it

The button shows only on a discovered tab with an entry in experimentalHelp.js. It opens a modal
dialog titled with the tab name: focus moves to Close, Tab stays inside, Escape or Close returns
focus to the button, and switching tab closes it. close is a new shell string in all 11 locales.
This commit is contained in:
Doxa Maps 2026-09-28 17:29:27 +00:00
parent d8364d949a
commit addf520dbe
2 changed files with 53 additions and 11 deletions

View file

@ -151,6 +151,23 @@
nothing switched, not to block the app. Sibling of the panes, not inside one, so it survives
whichever tab is currently active. -->
<div v-if="disabledNotice" class="dms-notice" role="status">{{ disabledNotice }}</div>
<!-- THE EXPERIMENTAL "?" — what a shelf map is for and how to use it (src/experimentalHelp.js).
Shown only on a discovered tab that HAS an entry: a map with no text shows no button rather
than an empty dialog. Modal: focus moves to Close, Tab stays inside, Escape or Close returns
focus to the "?", and switching tab closes it. -->
<button v-if="activeHelp" ref="helpBtn" type="button" class="dms-help-btn"
:aria-label="tr('help')" :title="tr('help')" @click="openHelp">?</button>
<div v-if="helpOpen && activeHelp" class="dms-help-scrim" @click.self="closeHelp"
@keydown.esc.stop.prevent="closeHelp">
<div class="dms-help" role="dialog" aria-modal="true" aria-labelledby="dms-help-title"
@keydown.tab.prevent="$refs.helpClose && $refs.helpClose.focus()">
<h2 id="dms-help-title" class="dms-help-title">{{ tabLabel(tabs[activeIndex]) }}</h2>
<p class="dms-help-what"><strong>{{ activeHelp.what }}</strong></p>
<p class="dms-help-how">{{ activeHelp.how }}</p>
<button ref="helpClose" type="button" class="dms-help-close" @click="closeHelp">{{ tr('close') }}</button>
</div>
</div>
</VerticalTabsLayout>
</template>
@ -159,6 +176,7 @@ import { defineComponent, inject } from 'vue'
import VerticalTabsLayout from '@map/layouts/VerticalTabsLayout.vue'
import { SWITCHER_LANGUAGES, loadLocale, saveLocale, getLanguage } from '@map/layouts/app-wrapper-material/languages.js'
import { translate, LOCALES, hasKey } from './src/strings.js'
import { helpFor } from './src/experimentalHelp.js'
import { loadScriptOnce, whenTagDefined } from '@map/utils/embedScript.js'
// THE HOST PAGE'S LANGUAGE, by the same ladder every nested map already uses (library/i18n
@ -764,6 +782,7 @@ export default defineComponent({
// onSelect's intercept). '' renders nothing; _noticeTimer clears any earlier message's timeout
// so two quick clicks don't fight over when it disappears.
disabledNotice: '', _noticeTimer: null,
helpOpen: false,
// Set when EVERY manifest candidate failed. The manifest carries each tab's scriptUrl, so
// without it no src-composed tab can mount; this turns that into a visible pane message
// instead of an empty grey rectangle. See loadManifest().
@ -800,6 +819,12 @@ export default defineComponent({
}
},
computed: {
// The active tab's "?" text, or null: only discovered (Experimental) tabs, and only when
// experimentalHelp.js has an entry for that bundle in this locale (English fallback inside).
activeHelp() {
const t = this.tabs[this.activeIndex]
return t && t.discovered ? helpFor(t.id, this.uiLocale) : null
},
// Merge profile-config (string or object) over the discrete props, so a host can pass either.
cfg() {
let pc = this.profileConfig
@ -959,6 +984,7 @@ export default defineComponent({
this._mql = null; this._onMedia = null
},
watch: {
activeIndex() { this.helpOpen = false },
// When the parent page changes the inherited `language` prop at runtime, re-drive the CURRENTLY
// visible tab into the new language by re-setting its element's profile-config.locale (which is
// languages.js's contract). Other tabs pick it up lazily on their next open, since mountElement
@ -979,6 +1005,8 @@ export default defineComponent({
tab(id) { if (id) this.onSelect(id) }
},
methods: {
openHelp() { this.helpOpen = true; this.$nextTick(() => this.$refs.helpClose && this.$refs.helpClose.focus()) },
closeHelp() { this.helpOpen = false; this.$nextTick(() => this.$refs.helpBtn && this.$refs.helpBtn.focus()) },
setSlot(i, el) { if (el) this._slots = Object.assign(this._slots || {}, { [i]: el }) },
// The gallery's own words in uiLocale (src/strings.js). `t` in the template is shadowed by the
// v-for tab variable inside the panes, so those use `tr`; both are the same function.
@ -1494,6 +1522,20 @@ export default defineComponent({
/* THE DEVICE-RULE NOTICE — the transient "desktop only" toast for the disabled Big Picture row on
mobile. Material light only (surface/border/text/muted tokens), matching the rest of this file. */
.dms-help-btn {
position: fixed; right: 16px; bottom: 24px; z-index: 40; width: 36px; height: 36px; border-radius: 50%;
font: 700 17px/1 Georgia, serif; color: #3b463d; background: #fff; border: 1px solid #d1d4cd;
box-shadow: 0 4px 12px rgba(0,0,0,.12); cursor: pointer;
}
.dms-help-btn:focus-visible, .dms-help-close:focus-visible { outline: 2px solid #3f8f5a; outline-offset: 2px; }
.dms-help-scrim { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,.35); padding: 16px; }
.dms-help {
max-width: 420px; width: 100%; color: #3b463d; background: #fff; border-radius: 12px;
padding: 18px 20px 14px; box-shadow: 0 12px 32px rgba(0,0,0,.2); font-size: 14px; line-height: 1.5;
}
.dms-help-title { margin: 0 0 8px; font-size: 16px; }
.dms-help-what, .dms-help-how { margin: 0 0 10px; }
.dms-help-close { float: right; font: inherit; padding: 6px 14px; border-radius: 8px; border: 1px solid #d1d4cd; background: #f3f3f1; cursor: pointer; }
.dms-notice {
position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 40;
max-width: 320px; text-align: center; font-size: 13px; line-height: 1.4;

View file

@ -27,7 +27,7 @@ export const STRINGS = {
'tab.upg-100': 'UPG-100 List', 'sub.upg-100': 'my unreached list + posters',
navLabel: 'Research maps', meta: '{n} maps', loading: 'Loading {label}…', loadFailed: 'Could not load {label}.',
manifestHint: 'This tab is composed from manifest.json — build the tree, or serve the built tree, to load it.',
help: 'About this map', desktopOnly: 'desktop only', lab: 'lab', experimentalSub: 'experimental bundle',
close: 'Close', help: 'About this map', desktopOnly: 'desktop only', lab: 'lab', experimentalSub: 'experimental bundle',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/en/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Toggle list", openViews: "Open the list of views", expandList: "Expand list", collapseList: "Collapse list", collapse: "Collapse",
@ -47,7 +47,7 @@ export const STRINGS = {
'tab.upg-100': 'Lista UPG-100', 'sub.upg-100': 'mi lista de no alcanzados + carteles',
navLabel: 'Mapas de investigación', meta: '{n} mapas', loading: 'Cargando {label}…', loadFailed: 'No se pudo cargar {label}.',
manifestHint: 'Esta pestaña se compone a partir de manifest.json: compila el árbol o sirve el árbol compilado para cargarla.',
help: 'Acerca de este mapa', desktopOnly: 'solo escritorio', lab: 'beta', experimentalSub: 'mapa experimental',
close: 'Cerrar', help: 'Acerca de este mapa', desktopOnly: 'solo escritorio', lab: 'beta', experimentalSub: 'mapa experimental',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/es/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Mostrar u ocultar la lista", openViews: "Abrir la lista de vistas", expandList: "Expandir la lista", collapseList: "Contraer la lista", collapse: "Contraer",
@ -67,7 +67,7 @@ export const STRINGS = {
'tab.upg-100': 'Liste UPG-100', 'sub.upg-100': 'ma liste des non atteints + affiches',
navLabel: 'Cartes de recherche', meta: '{n} cartes', loading: 'Chargement de {label}…', loadFailed: 'Impossible de charger {label}.',
manifestHint: "Cet onglet est composé à partir de manifest.json : construisez l'arborescence ou servez l'arborescence construite pour le charger.",
help: 'À propos de cette carte', desktopOnly: 'ordinateur uniquement', lab: 'bêta', experimentalSub: 'carte expérimentale',
close: 'Fermer', help: 'À propos de cette carte', desktopOnly: 'ordinateur uniquement', lab: 'bêta', experimentalSub: 'carte expérimentale',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/fr/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Afficher ou masquer la liste", openViews: "Ouvrir la liste des vues", expandList: "Déplier la liste", collapseList: "Replier la liste", collapse: "Replier",
@ -87,7 +87,7 @@ export const STRINGS = {
'tab.upg-100': 'UPG-100-Liste', 'sub.upg-100': 'meine Liste der Unerreichten + Poster',
navLabel: 'Forschungskarten', meta: '{n} Karten', loading: '{label} wird geladen…', loadFailed: '{label} konnte nicht geladen werden.',
manifestHint: 'Dieser Tab wird aus manifest.json zusammengesetzt – bauen Sie den Baum oder stellen Sie den gebauten Baum bereit, um ihn zu laden.',
help: 'Über diese Karte', desktopOnly: 'nur Desktop', lab: 'Beta', experimentalSub: 'experimentelle Karte',
close: 'Schließen', help: 'Über diese Karte', desktopOnly: 'nur Desktop', lab: 'Beta', experimentalSub: 'experimentelle Karte',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/de/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Liste ein-/ausblenden", openViews: "Ansichtsliste öffnen", expandList: "Liste ausklappen", collapseList: "Liste einklappen", collapse: "Einklappen",
@ -107,7 +107,7 @@ export const STRINGS = {
'tab.upg-100': 'Lista UPG-100', 'sub.upg-100': 'minha lista de não alcançados + cartazes',
navLabel: 'Mapas de pesquisa', meta: '{n} mapas', loading: 'Carregando {label}…', loadFailed: 'Não foi possível carregar {label}.',
manifestHint: 'Esta aba é composta a partir do manifest.json — compile a árvore ou sirva a árvore compilada para carregá-la.',
help: 'Sobre este mapa', desktopOnly: 'só no computador', lab: 'beta', experimentalSub: 'mapa experimental',
close: 'Fechar', help: 'Sobre este mapa', desktopOnly: 'só no computador', lab: 'beta', experimentalSub: 'mapa experimental',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/pt/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Mostrar ou ocultar a lista", openViews: "Abrir a lista de visualizações", expandList: "Expandir a lista", collapseList: "Recolher a lista", collapse: "Recolher",
@ -129,7 +129,7 @@ export const STRINGS = {
'tab.upg-100': 'Lista UPG-100', 'sub.upg-100': 'la mia lista dei non raggiunti + poster',
navLabel: 'Mappe di ricerca', meta: '{n} mappe', loading: 'Caricamento di {label}…', loadFailed: 'Impossibile caricare {label}.',
manifestHint: "Questa scheda è composta da manifest.json: compila l'albero o pubblica l'albero compilato per caricarla.",
help: 'Informazioni su questa mappa', desktopOnly: 'solo desktop', lab: 'beta', experimentalSub: 'mappa sperimentale',
close: 'Chiudi', help: 'Informazioni su questa mappa', desktopOnly: 'solo desktop', lab: 'beta', experimentalSub: 'mappa sperimentale',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/it/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Mostra o nascondi l’elenco", openViews: "Apri l’elenco delle viste", expandList: "Espandi l’elenco", collapseList: "Comprimi l’elenco", collapse: "Comprimi",
@ -149,7 +149,7 @@ export const STRINGS = {
'tab.upg-100': 'Lista UPG-100', 'sub.upg-100': 'lista mea de neatinși + afișe',
navLabel: 'Hărți de cercetare', meta: '{n} hărți', loading: 'Se încarcă {label}…', loadFailed: '{label} nu a putut fi încărcat.',
manifestHint: 'Această filă este compusă din manifest.json — construiți arborele sau serviți arborele construit pentru a o încărca.',
help: 'Despre această hartă', desktopOnly: 'doar pe desktop', lab: 'beta', experimentalSub: 'hartă experimentală',
close: 'Închide', help: 'Despre această hartă', desktopOnly: 'doar pe desktop', lab: 'beta', experimentalSub: 'hartă experimentală',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/ro/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Afișează sau ascunde lista", openViews: "Deschide lista de vizualizări", expandList: "Extinde lista", collapseList: "Restrânge lista", collapse: "Restrânge",
@ -169,7 +169,7 @@ export const STRINGS = {
'tab.upg-100': 'Список UPG-100', 'sub.upg-100': 'мой список недостигнутых + плакаты',
navLabel: 'Исследовательские карты', meta: 'Карт: {n}', loading: 'Загрузка: {label}…', loadFailed: 'Не удалось загрузить: {label}.',
manifestHint: 'Эта вкладка собирается из manifest.json — соберите дерево или разместите собранное дерево, чтобы загрузить её.',
help: 'Об этой карте', desktopOnly: 'только на компьютере', lab: 'бета', experimentalSub: 'экспериментальная карта',
close: 'Закрыть', help: 'Об этой карте', desktopOnly: 'только на компьютере', lab: 'бета', experimentalSub: 'экспериментальная карта',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/ru/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "Показать или скрыть список", openViews: "Открыть список представлений", expandList: "Развернуть список", collapseList: "Свернуть список", collapse: "Свернуть",
@ -189,7 +189,7 @@ export const STRINGS = {
'tab.upg-100': 'قائمة UPG-100', 'sub.upg-100': 'قائمتي لغير المُبشَّرين + ملصقات',
navLabel: 'خرائط البحث', meta: 'عدد الخرائط: {n}', loading: 'جارٍ تحميل {label}…', loadFailed: 'تعذّر تحميل {label}.',
manifestHint: 'تُبنى علامة التبويب هذه من manifest.json — ابنِ الشجرة أو قدّم الشجرة المبنية لتحميلها.',
help: 'عن هذه الخريطة', desktopOnly: 'على الحاسوب فقط', lab: 'تجريبي', experimentalSub: 'خريطة تجريبية',
close: 'إغلاق', help: 'عن هذه الخريطة', desktopOnly: 'على الحاسوب فقط', lab: 'تجريبي', experimentalSub: 'خريطة تجريبية',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/ar/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "إظهار القائمة أو إخفاؤها", openViews: "فتح قائمة العروض", expandList: "توسيع القائمة", collapseList: "طي القائمة", collapse: "طي",
@ -209,7 +209,7 @@ export const STRINGS = {
'tab.upg-100': 'UPG-100 सूची', 'sub.upg-100': 'मेरी अप्राप्त सूची + पोस्टर',
navLabel: 'शोध मानचित्र', meta: '{n} मानचित्र', loading: '{label} लोड हो रहा है…', loadFailed: '{label} लोड नहीं हो सका।',
manifestHint: 'यह टैब manifest.json से बनता है — इसे लोड करने के लिए ट्री बनाएँ या बना हुआ ट्री सर्व करें।',
help: 'इस नक्शे के बारे में', desktopOnly: 'केवल डेस्कटॉप', lab: 'बीटा', experimentalSub: 'प्रायोगिक मानचित्र',
close: 'बंद करें', help: 'इस नक्शे के बारे में', desktopOnly: 'केवल डेस्कटॉप', lab: 'बीटा', experimentalSub: 'प्रायोगिक मानचित्र',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/hi/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "सूची दिखाएँ या छिपाएँ", openViews: "दृश्यों की सूची खोलें", expandList: "सूची फैलाएँ", collapseList: "सूची संक्षिप्त करें", collapse: "संक्षिप्त करें",
@ -229,7 +229,7 @@ export const STRINGS = {
'tab.upg-100': 'UPG-100 名单', 'sub.upg-100': '我的未得之民名单 + 海报',
navLabel: '研究地图', meta: '{n} 张地图', loading: '正在加载{label}…', loadFailed: '无法加载{label}。',
manifestHint: '此标签页由 manifest.json 组成——请构建目录树或提供已构建的目录树以加载它。',
help: '关于此地图', desktopOnly: '仅限桌面', lab: '测试版', experimentalSub: '实验性地图',
close: '关闭', help: '关于此地图', desktopOnly: '仅限桌面', lab: '测试版', experimentalSub: '实验性地图',
// The shared VerticalTabsLayout's own controls, VERBATIM from library/i18n/locales/zh/studio.json
// (plugin studio's catalog) so both shells name the same buttons the same way.
toggleList: "显示或隐藏列表", openViews: "打开视图列表", expandList: "展开列表", collapseList: "折叠列表", collapse: "折叠",