One phone flag per map, and the leaf page follows it as well as the Gallery
A map says how it behaves on a narrow screen once, in its <profile>.meta.json: "mobile": "desktop-only" (blocked on a phone) or "larger-screens" (opens, under a dismissible notice). Absent means phone-ready. Until now the rule lived only in the Research Gallery, as two hard-coded id sets, so a map opened standalone on its own leaf page on a phone got neither the block nor the notice. - profile-meta.mjs knows the vocabulary; generate-tree and the dev manifest carry the flag into each profile's manifest entry, and generate-tree into the leaf page's BASE_CONFIG (with the map's name for the desktop-only sentence). - ProfileLoader mounts library/components/LargerScreenNotice.vue on a screen under 640px: for desktop-only the profile is never mounted and a centred message says why; for larger-screens a slim in-flow bar sits above the map (it cannot cover the map's controls), dismissible per profile for the session, in the element's own locale and direction. Inside the Gallery it stays quiet: every Gallery pane now carries data-doxa-gallery. - One text: the Gallery's two sentences moved into the library catalogue as messages.largerScreenNotice / desktopOnlyNotice (+ noticeDismiss), all 11 locales moved as they were, and the Gallery reads them from there. - The Gallery derives its rules from the manifest's flags. On a phone it mounts nothing until the manifest has landed: its baked first tab (Big Picture) would otherwise mount there before the flags say it is desktop-only. - dashboard-plus-grid is desktop-only and gospel-internet/board is larger-screens, the two sets the Gallery held; nothing changes for either. - check:mobile-gating now asserts the vocabulary, that each flagged leaf page carries its flag, the Gallery's derivation and its closed-before-manifest rule, that Big Picture stays desktop-only, the 11 locales, and the notice's mounting. Its self-test plants one failure per rule.
This commit is contained in:
parent
b07fc59e0b
commit
51ef6f7ec4
34 changed files with 444 additions and 131 deletions
|
|
@ -16,6 +16,8 @@ and a change here can break other maps.
|
|||
|
||||
- **BackToTopButton** — BackToTopButton — a ↑ that appears once a long list has been scrolled, and sits NEXT TO the
|
||||
`import … from '@map/components/BackToTopButton.vue'`
|
||||
- **LargerScreenNotice** — LargerScreenNotice — what a map says on a phone when its <profile>.meta.json declares mobile
|
||||
`import … from '@map/components/LargerScreenNotice.vue'`
|
||||
- **PeopleGroupDetail** — (no description)
|
||||
`import … from '@map/components/PeopleGroupDetail.vue'`
|
||||
- **ResearchMapFilterPanel** — ResearchMapFilterPanel.vue — Multi-field filter UI for the research-map.
|
||||
|
|
|
|||
|
|
@ -1,79 +1,150 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-mobile-gating.mjs — the SPA's device rules stay on the right shelves (DRV1650-MOBILE-GATING).
|
||||
* check-mobile-gating.mjs — one phone flag per map, and every surface follows it (SPEC-MOBILE-FLAG,
|
||||
* the per-plugin half of DRV1650-MOBILE-GATING).
|
||||
*
|
||||
* DESKTOP_ONLY production rows BLOCKED on a narrow screen (disabled + "desktop only" + a message)
|
||||
* MOBILE_NOTICE experimental rows that OPEN on a narrow screen, with a "not built for mobile" notice
|
||||
*
|
||||
* Headless: reads plugin-designs/doxa-maps-spa/doxa-maps-spa.vue and src/strings.js. It fails if:
|
||||
* · a DESKTOP_ONLY id is not a curated (production) tab, or a MOBILE_NOTICE id is not an Experimental
|
||||
* bundle (curated, excluded, or no plugin folder), or an id is in both sets;
|
||||
* · the predicates stop reading their set and the narrow flag, or the pane notice stops rendering;
|
||||
* · any locale lacks mobileNotBuilt / mobileNotice / dismiss / desktopNotice, or desktopNotice
|
||||
* stops naming the map ({label}), which it must now that more than one map can be desktop-only.
|
||||
* `--self-test` proves every rule can fail on a planted bad case before trusting a pass.
|
||||
* A map declares how it behaves on a narrow screen ONCE, in <profile>.meta.json:
|
||||
* "mobile": "desktop-only" blocked on a phone (the Gallery disables its row; the leaf shows why)
|
||||
* "mobile": "larger-screens" opens on a phone, under a dismissible notice
|
||||
* absent phone-ready
|
||||
* generate-tree carries it into the manifest and the leaf page's BASE_CONFIG; the Research Gallery
|
||||
* derives its rules from the manifest; ProfileLoader shows the leaf's notice. This fails if:
|
||||
* (a) a meta.json declares a value outside that vocabulary (a typo must not read as phone-ready);
|
||||
* (b) a flagged profile's leaf page does not carry the flag, or a leaf carries one its meta dropped;
|
||||
* (c) the Gallery hard-codes an id set again, stops deriving from the manifest, or stops failing
|
||||
* closed on a phone before the manifest lands (its baked first tab would mount there);
|
||||
* (d) dashboard-plus-grid stops being desktop-only — the migration must not soften that gate;
|
||||
* (e) a locale loses one of the notice sentences, or the desktop-only one stops naming the map;
|
||||
* (f) ProfileLoader stops mounting the notice, or stops staying quiet inside the Gallery.
|
||||
* `--self-test` plants one bad case per rule and proves each can fail before a pass is trusted.
|
||||
*/
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { MOBILE } from './profile-meta.mjs'
|
||||
import { TREE_NAME } from './_tree-name.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const SPA = join(ROOT, 'plugin-designs/doxa-maps-spa/doxa-maps-spa.vue')
|
||||
const STRINGS = join(ROOT, 'plugin-designs/doxa-maps-spa/src/strings.js')
|
||||
const KEYS = ['mobileNotBuilt', 'mobileNotice', 'dismiss', 'desktopNotice']
|
||||
const LOADER = join(ROOT, 'library/ProfileLoader.vue')
|
||||
const SPA_KEYS = ['mobileNotBuilt', 'dismiss']
|
||||
const LIB_KEYS = ['largerScreenNotice', 'desktopOnlyNotice', 'noticeDismiss']
|
||||
|
||||
const setOf = (src, name) => {
|
||||
const m = src.match(new RegExp(`const ${name} = new Set\\(\\[([^\\]]*)\\]\\)`))
|
||||
return m ? new Set([...m[1].matchAll(/'([a-z0-9-]+)'/g)].map((x) => x[1])) : null
|
||||
}
|
||||
|
||||
export function checkGating(src, locales, hasPlugin) {
|
||||
export function checkGating({ metas, leaves, spaSrc, spaLocales, libLocales, loaderSrc }) {
|
||||
const errs = []
|
||||
const desk = setOf(src, 'DESKTOP_ONLY'), mob = setOf(src, 'MOBILE_NOTICE'), notExp = setOf(src, 'NOT_EXPERIMENTAL')
|
||||
if (!desk) errs.push('DESKTOP_ONLY set not found')
|
||||
if (!mob) errs.push('MOBILE_NOTICE set not found')
|
||||
if (!notExp) errs.push('NOT_EXPERIMENTAL set not found')
|
||||
if (errs.length) return errs
|
||||
const curated = new Set([...src.matchAll(/bundle: '([a-z0-9-]+)'/g)].map((x) => x[1]))
|
||||
for (const id of desk) if (!curated.has(id)) errs.push(`DESKTOP_ONLY "${id}" is not a curated production tab`)
|
||||
for (const id of mob) {
|
||||
if (curated.has(id)) errs.push(`MOBILE_NOTICE "${id}" is a curated production tab (production maps are blocked, not warned)`)
|
||||
if (notExp.has(id)) errs.push(`MOBILE_NOTICE "${id}" is excluded from the Experimental shelf`)
|
||||
if (!hasPlugin(id)) errs.push(`MOBILE_NOTICE "${id}" has no plugin-designs/${id} folder`)
|
||||
if (desk.has(id)) errs.push(`"${id}" is in BOTH sets`)
|
||||
// (a) the vocabulary
|
||||
for (const m of metas) {
|
||||
if (m.mobile !== undefined && !MOBILE.includes(m.mobile)) errs.push(`${m.bundle}/${m.profile}.meta.json: mobile "${m.mobile}" is not one of ${MOBILE.join(' | ')}`)
|
||||
}
|
||||
if (!/isDesktopOnly\(id\)\s*\{\s*return DESKTOP_ONLY\.has\(id\) && this\.narrow/.test(src)) errs.push('isDesktopOnly no longer reads DESKTOP_ONLY + narrow')
|
||||
if (!/showsMobileNotice\(id\)\s*\{\s*return MOBILE_NOTICE\.has\(id\) && this\.narrow/.test(src)) errs.push('showsMobileNotice no longer reads MOBILE_NOTICE + narrow')
|
||||
if (!/v-if="showsMobileNotice\(t\.id\)"[^>]*role="status"/.test(src)) errs.push('the pane notice (showsMobileNotice, role=status) is not rendered')
|
||||
for (const [loc, t] of Object.entries(locales || {})) {
|
||||
for (const k of KEYS) if (!t || !String(t[k] || '').trim()) errs.push(`strings ${loc}: missing ${k}`)
|
||||
if (t && t.desktopNotice && !t.desktopNotice.includes('{label}')) errs.push(`strings ${loc}: desktopNotice does not name the map ({label})`)
|
||||
// (b) the leaf pages carry exactly what the metas declare
|
||||
for (const m of metas) {
|
||||
const key = `${m.bundle}/${m.profile}`
|
||||
if (!(key in leaves)) continue // no leaf page in this tree: nothing to carry
|
||||
const got = leaves[key] ? leaves[key].mobile : undefined
|
||||
const want = MOBILE.includes(m.mobile) ? m.mobile : undefined
|
||||
if (got !== want) errs.push(`${key}: the leaf page says mobile ${JSON.stringify(got)}, its meta.json says ${JSON.stringify(want)} (regenerate the tree)`)
|
||||
}
|
||||
if (!locales || Object.keys(locales).length < 11) errs.push(`strings: ${Object.keys(locales || {}).length} locales, expected 11`)
|
||||
// (c) the Gallery derives, and fails closed
|
||||
if (/const (DESKTOP_ONLY|MOBILE_NOTICE) = new Set\(/.test(spaSrc)) errs.push('the Gallery hard-codes a DESKTOP_ONLY / MOBILE_NOTICE id set again; the flag lives in meta.json')
|
||||
if (!/this\.mobileFlags = mobileFlagsFromManifest\(m\)/.test(spaSrc)) errs.push('applyManifest no longer records the flags from the manifest')
|
||||
if (!/isDesktopOnly\(id\)\s*\{\s*return this\.narrow && this\.mobileFlagOf\(id\) === 'desktop-only'/.test(spaSrc)) errs.push("isDesktopOnly no longer reads narrow + the 'desktop-only' flag")
|
||||
if (!/showsMobileNotice\(id\)\s*\{\s*return this\.narrow && this\.mobileFlagOf\(id\) === 'larger-screens'/.test(spaSrc)) errs.push("showsMobileNotice no longer reads narrow + the 'larger-screens' flag")
|
||||
if (!/if \(this\.narrow && this\.mobileFlags === null\) return/.test(spaSrc)) errs.push('select() no longer waits for the flags on a phone: the baked first tab would mount before the manifest says it is desktop-only')
|
||||
if (!/v-if="showsMobileNotice\(t\.id\)"[^>]*role="status"/.test(spaSrc)) errs.push('the pane notice (showsMobileNotice, role=status) is not rendered')
|
||||
if (!/data-doxa-gallery=""/.test(spaSrc)) errs.push('Gallery panes no longer carry data-doxa-gallery: a map inside would show a second notice')
|
||||
// (d) the one gate the migration must not soften
|
||||
const dpg = metas.find((m) => m.bundle === 'dashboard-plus-grid' && m.profile === 'dashboard-plus-grid')
|
||||
if (!dpg || dpg.mobile !== 'desktop-only') errs.push('dashboard-plus-grid is no longer "mobile": "desktop-only" (Big Picture is blocked on phones)')
|
||||
// (e) the sentences
|
||||
for (const [loc, t] of Object.entries(spaLocales || {})) {
|
||||
for (const k of SPA_KEYS) if (!t || !String(t[k] || '').trim()) errs.push(`Gallery strings ${loc}: missing ${k}`)
|
||||
}
|
||||
if (Object.keys(spaLocales || {}).length < 11) errs.push(`Gallery strings: ${Object.keys(spaLocales || {}).length} locales, expected 11`)
|
||||
for (const [loc, t] of Object.entries(libLocales || {})) {
|
||||
for (const k of LIB_KEYS) if (!t || !String(t[k] || '').trim()) errs.push(`library messages ${loc}: missing ${k}`)
|
||||
if (t && t.desktopOnlyNotice && !t.desktopOnlyNotice.includes('{label}')) errs.push(`library messages ${loc}: desktopOnlyNotice does not name the map ({label})`)
|
||||
}
|
||||
if (Object.keys(libLocales || {}).length < 11) errs.push(`library messages: ${Object.keys(libLocales || {}).length} locales, expected 11`)
|
||||
// (f) the leaf's own notice
|
||||
if (!/<LargerScreenNotice v-else-if="blockedOnThisScreen"/.test(loaderSrc)) errs.push('ProfileLoader no longer replaces a desktop-only profile with the notice on a phone')
|
||||
if (!/<LargerScreenNotice v-if="adviseOnThisScreen"/.test(loaderSrc)) errs.push('ProfileLoader no longer shows the larger-screens notice')
|
||||
if (!/hasAttribute\('data-doxa-gallery'\)/.test(loaderSrc)) errs.push('ProfileLoader no longer stays quiet inside the Gallery (data-doxa-gallery)')
|
||||
return errs
|
||||
}
|
||||
|
||||
async function loadLocales() {
|
||||
function readMetas() {
|
||||
const out = []
|
||||
const base = join(ROOT, 'plugin-designs')
|
||||
for (const bundle of readdirSync(base)) {
|
||||
let files = []
|
||||
try { files = readdirSync(join(base, bundle)).filter((f) => f.endsWith('.meta.json')) } catch { continue }
|
||||
for (const f of files) {
|
||||
try {
|
||||
const meta = JSON.parse(readFileSync(join(base, bundle, f), 'utf8'))
|
||||
out.push({ bundle, profile: f.replace(/\.meta\.json$/, ''), mobile: meta.mobile })
|
||||
} catch { /* an unreadable meta is other gates' business */ }
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// A leaf page's BASE_CONFIG, or null when the page has none. Keyed "bundle/profile"; only pages that exist.
|
||||
function readLeaves(metas) {
|
||||
const out = {}
|
||||
for (const m of metas) {
|
||||
const page = join(ROOT, 'public', TREE_NAME, m.bundle, m.profile, 'index.html')
|
||||
if (!existsSync(page)) continue
|
||||
const hit = readFileSync(page, 'utf8').match(/var BASE_CONFIG = (\{.*?\});/)
|
||||
try { out[`${m.bundle}/${m.profile}`] = hit ? JSON.parse(hit[1]) : null } catch { out[`${m.bundle}/${m.profile}`] = null }
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async function readSpaLocales() {
|
||||
const mod = await import(pathToFileURL(STRINGS).href)
|
||||
return Object.values(mod).find((v) => v && typeof v === 'object' && v.en) || {}
|
||||
}
|
||||
|
||||
function readLibLocales() {
|
||||
const out = {}
|
||||
const base = join(ROOT, 'library/i18n/locales')
|
||||
for (const lc of readdirSync(base, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)) {
|
||||
try { out[lc] = JSON.parse(readFileSync(join(base, lc, 'messages.json'), 'utf8')) } catch { out[lc] = null }
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const metas = readMetas()
|
||||
const real = {
|
||||
metas,
|
||||
leaves: readLeaves(metas),
|
||||
spaSrc: readFileSync(SPA, 'utf8'),
|
||||
spaLocales: await readSpaLocales(),
|
||||
libLocales: readLibLocales(),
|
||||
loaderSrc: readFileSync(LOADER, 'utf8'),
|
||||
}
|
||||
|
||||
if (process.argv.includes('--self-test')) {
|
||||
const src = readFileSync(SPA, 'utf8'), locales = await loadLocales()
|
||||
const yes = () => true
|
||||
const flagged = real.metas.find((m) => m.mobile && `${m.bundle}/${m.profile}` in real.leaves)
|
||||
const withMeta = (fn) => ({ ...real, metas: real.metas.map((m) => fn({ ...m })) })
|
||||
const cases = [
|
||||
['a clean tree passes', src, locales, yes, 0],
|
||||
['a curated id in MOBILE_NOTICE', src.replace("new Set(['gospel-internet'])", "new Set(['gospel-internet', 'dashboard-plus-grid'])"), locales, yes, 1],
|
||||
['an experimental id in DESKTOP_ONLY', src.replace("new Set(['dashboard-plus-grid'])", "new Set(['dashboard-plus-grid', 'gospel-internet'])"), locales, yes, 1],
|
||||
['a MOBILE_NOTICE id with no plugin', src, locales, () => false, 1],
|
||||
['the predicate stops reading narrow', src.replace('return DESKTOP_ONLY.has(id) && this.narrow', 'return DESKTOP_ONLY.has(id)'), locales, yes, 1],
|
||||
['the pane notice is removed', src.replace('v-if="showsMobileNotice(t.id)"', 'v-if="false"'), locales, yes, 1],
|
||||
['a locale loses mobileNotice', src, { ...locales, ar: { ...locales.ar, mobileNotice: '' } }, yes, 1],
|
||||
['desktopNotice stops naming the map', src, { ...locales, zh: { ...locales.zh, desktopNotice: '仅支持桌面端' } }, yes, 1],
|
||||
['a clean tree passes', real, 0],
|
||||
['(a) a typo in the vocabulary', withMeta((m) => (m.bundle === 'gospel-internet' && m.profile === 'board' ? { ...m, mobile: 'larger-screen' } : m)), 1],
|
||||
['(b) a flagged leaf page without the flag', flagged ? { ...real, leaves: { ...real.leaves, [`${flagged.bundle}/${flagged.profile}`]: {} } } : null, 1],
|
||||
['(b) a leaf page carrying a flag its meta dropped', flagged ? withMeta((m) => (m === flagged || (m.bundle === flagged.bundle && m.profile === flagged.profile) ? { ...m, mobile: undefined } : m)) : null, 1],
|
||||
['(c) the id set comes back', { ...real, spaSrc: real.spaSrc + "\nconst DESKTOP_ONLY = new Set(['dashboard-plus-grid'])" }, 1],
|
||||
['(c) select() stops waiting for the flags', { ...real, spaSrc: real.spaSrc.replace('if (this.narrow && this.mobileFlags === null) return', '') }, 1],
|
||||
['(c) the pane notice is removed', { ...real, spaSrc: real.spaSrc.replace('v-if="showsMobileNotice(t.id)"', 'v-if="false"') }, 1],
|
||||
['(d) Big Picture is softened', withMeta((m) => (m.bundle === 'dashboard-plus-grid' ? { ...m, mobile: 'larger-screens' } : m)), 1],
|
||||
['(e) a locale loses the notice', { ...real, libLocales: { ...real.libLocales, ar: { ...real.libLocales.ar, largerScreenNotice: '' } } }, 1],
|
||||
['(e) the desktop-only sentence stops naming the map', { ...real, libLocales: { ...real.libLocales, zh: { ...real.libLocales.zh, desktopOnlyNotice: '仅支持桌面端' } } }, 1],
|
||||
['(f) ProfileLoader stops blocking', { ...real, loaderSrc: real.loaderSrc.replace('v-else-if="blockedOnThisScreen"', 'v-else-if="false"') }, 1],
|
||||
]
|
||||
let ok = true
|
||||
for (const [name, s, l, hp, want] of cases) {
|
||||
const n = checkGating(s, l, hp).length
|
||||
for (const [name, input, want] of cases) {
|
||||
if (!input) { console.log(` - ${name}: no flagged leaf page in this tree to plant it on`); ok = false; continue }
|
||||
const n = checkGating(input).length
|
||||
const pass = want === 0 ? n === 0 : n >= want
|
||||
if (!pass) ok = false
|
||||
console.log(` ${pass ? '✓' : '✗'} ${name} -> ${n} error(s) (want ${want === 0 ? 0 : '≥' + want})`)
|
||||
|
|
@ -82,10 +153,10 @@ if (process.argv.includes('--self-test')) {
|
|||
process.exit(ok ? 0 : 1)
|
||||
}
|
||||
|
||||
const src = readFileSync(SPA, 'utf8')
|
||||
const errs = checkGating(src, await loadLocales(), (id) => existsSync(join(ROOT, 'plugin-designs', id)))
|
||||
const errs = checkGating(real)
|
||||
if (errs.length) {
|
||||
console.error('MOBILE-GATING: FAIL\n' + errs.map((e) => ' ✗ ' + e).join('\n'))
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`MOBILE-GATING: PASS — desktop-only: ${[...setOf(src, 'DESKTOP_ONLY')].join(', ')} · mobile notice: ${[...setOf(src, 'MOBILE_NOTICE')].join(', ')} · 11 locales`)
|
||||
const list = (flag) => real.metas.filter((m) => m.mobile === flag).map((m) => `${m.bundle}/${m.profile}`).join(', ') || '(none)'
|
||||
console.log(`MOBILE-GATING: PASS — desktop-only: ${list('desktop-only')} · larger-screens: ${list('larger-screens')} · leaf pages carry the flags · 11 locales`)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@
|
|||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// Driver set for the data bundler: handle every plugin generically, or the next author gets": 1,
|
||||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// RENAMED (Driver): \"Prayer Map\" → \"Prayer Progress\". His words: \"the prayer maps can be renamed": 1,
|
||||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// THE ONE DASHBOARD (the Driver's \"Big Picture\"): research-dashboard + people-groups-grid compiled": 1,
|
||||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// THE ONE ROW THE DEVICE RULE DISABLES. Driver, verbatim: \"it shouldn't change the tab order, it": 1,
|
||||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// The Driver's section/folder nav model, ported from app-wrapper-material/nav.js (the verified": 1,
|
||||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// The GROUPED rail model the shared VerticalTabsLayout paints, built from NAV_MODEL (the Driver's": 1,
|
||||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// Whether the standalone #toolbar locale dropdown renders. The Driver's rule: the cap-5 SPA has": 1,
|
||||
|
|
@ -41,7 +40,6 @@
|
|||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// source of the exact sections the Driver wants). Each entry is a GROUP for VerticalTabsLayout; each": 1,
|
||||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// stack-demo is broken and Driver-DEFERRED; a broken tab in the giveaway shelf reads as": 1,
|
||||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// the Driver has been looking at: not the plugin's content scroller, the PANE scrolling the": 1,
|
||||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// ── DEVICE RULES (Driver, 2026-09-28 16:50Z: DRV1650-MOBILE-GATING) ─────────────────────────": 1,
|
||||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// ★ FAIL-SOFT IS NOT FAIL-SILENT (BUG-SIMPLE-RESEARCH-GREY-PANE, Driver reported 4x).": 1,
|
||||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// ★ MOUNT THE COMPOSITE, NOT THE LEAF (Driver 2026-09-23: \"the Regions tab is supposed to be a": 1,
|
||||
"plugin-designs/doxa-maps-spa/doxa-maps-spa.vue\u0000// ★ THE 404 CLASS THIS REPLACED (measured 2026-09-22, hit by the Driver in his own browser). Every": 1,
|
||||
|
|
|
|||
|
|
@ -490,6 +490,18 @@ function readComposes(dir, profileName) {
|
|||
* `profile`/`tk`/`assetsBase`/`instanceId` are generator-owned and cannot be overridden.
|
||||
*/
|
||||
const RESERVED_CONFIG_KEYS = new Set(['profile', 'tk', 'assetsBase', 'instanceId', 'dataSource'])
|
||||
|
||||
/**
|
||||
* The phone rule for a standalone leaf (SPEC-MOBILE-FLAG): the manifest entry's `mobile` flag, and the
|
||||
* name the desktop-only message gives the map. ProfileLoader reads both; absent for a phone-ready map,
|
||||
* so every other leaf page keeps the bytes it has. The name is the profile's display label without a
|
||||
* trailing "(…)" note, which is written for the Studio rail rather than for a reader.
|
||||
*/
|
||||
function mobileConfig(profile) {
|
||||
if (!profile.mobile) return {}
|
||||
const name = String(profile.label || '').replace(/\s*\([^)]*\)\s*$/, '').trim()
|
||||
return { mobile: profile.mobile, ...(name ? { mobileLabel: name } : {}) }
|
||||
}
|
||||
function readProfileConfig(dir, profileName) {
|
||||
try {
|
||||
const metaPath = join(dir, `${profileName}.meta.json`)
|
||||
|
|
@ -1150,7 +1162,7 @@ function profileHtml(b, profile, tokenUrlExpr, bakedToken) {
|
|||
// Profile-declared keys first, so the generator-owned ones always win (they are
|
||||
// also filtered as reserved at read time — belt and braces, since a leaf whose
|
||||
// `profile` or `assetsBase` got overwritten would silently render the wrong thing).
|
||||
config: { ...(profile.extraConfig || {}), profile: profile.profile, dataSource: 'pray-tools', instanceId: `${b.bundle}-${profile.profile}`, assetsBase: assetsBaseFor(3) },
|
||||
config: { ...(profile.extraConfig || {}), ...mobileConfig(profile), profile: profile.profile, dataSource: 'pray-tools', instanceId: `${b.bundle}-${profile.profile}`, assetsBase: assetsBaseFor(3) },
|
||||
jsSrc: `../${b.bundle}.js`,
|
||||
apiBaseUrl: DEST_CONFIG.apiBaseUrl,
|
||||
tokenUrlExpr,
|
||||
|
|
@ -1184,7 +1196,7 @@ function instanceHtml(b, profile, inst, tokenUrlExpr, bakedToken) {
|
|||
// MERGE, never replace (go-int: a preset lost its profile's pinHint:false): the profile's
|
||||
// <profile>.meta.json `config` first, then the instance's props over it. The rule is stated in
|
||||
// documentation/PRESET-PLUGIN-STANDARD.md and asserted by check:preset-config-merge.
|
||||
config: { assetsBase: assetsBaseFor(4), ...(profile.extraConfig || {}), ...inst.props },
|
||||
config: { assetsBase: assetsBaseFor(4), ...(profile.extraConfig || {}), ...mobileConfig(profile), ...inst.props },
|
||||
jsSrc: `../../${b.bundle}.js`,
|
||||
apiBaseUrl: inst.apiBaseUrl || DEST_CONFIG.apiBaseUrl,
|
||||
tokenUrlExpr,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
* heavy true when the element holds live WebGL contexts
|
||||
* widget { corner } for a corner widget the Plugin Studio mounts once, fixed
|
||||
* visibility "team" | "public" default team — see VISIBILITY below
|
||||
* mobile "desktop-only" | "larger-screens" absent = phone-ready — see MOBILE below
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
|
@ -35,6 +36,19 @@ const CORNERS = ['bottom-right', 'bottom-left', 'top-right', 'top-left']
|
|||
*/
|
||||
export const VISIBILITY = ['team', 'public']
|
||||
|
||||
/**
|
||||
* ★ HOW A MAP BEHAVES ON A PHONE, declared once per profile (SPEC-MOBILE-FLAG). The Research Gallery
|
||||
* and the standalone leaf page both read it from here, through the manifest and the leaf's config:
|
||||
*
|
||||
* "desktop-only" BLOCKS on a narrow screen: the map is not mounted, a message says why
|
||||
* "larger-screens" ADVISES: the map opens, with a dismissible notice at its top
|
||||
* ABSENT phone-ready: nothing is shown
|
||||
*
|
||||
* Any other value is not coerced: check:mobile-gating fails on it, by profile, so a typo cannot
|
||||
* quietly turn a desktop-only map into a phone-ready one.
|
||||
*/
|
||||
export const MOBILE = ['desktop-only', 'larger-screens']
|
||||
|
||||
export function readProfileMeta(dir, profileName) {
|
||||
try {
|
||||
const meta = JSON.parse(readFileSync(join(dir, `${profileName}.meta.json`), 'utf8'))
|
||||
|
|
@ -54,8 +68,9 @@ export function readProfileMeta(dir, profileName) {
|
|||
visibilityInvalid: meta?.visibility === undefined || VISIBILITY.includes(meta?.visibility)
|
||||
? null
|
||||
: String(meta.visibility),
|
||||
mobile: MOBILE.includes(meta?.mobile) ? meta.mobile : null,
|
||||
}
|
||||
} catch { return { stage: 'experimental', heavy: false, widget: null, label: '', targets: null, visibility: 'team', visibilityInvalid: null } }
|
||||
} catch { return { stage: 'experimental', heavy: false, widget: null, label: '', targets: null, visibility: 'team', visibilityInvalid: null, mobile: null } }
|
||||
}
|
||||
|
||||
/** The optional manifest keys for one profile (stage always; heavy / widget only when set). */
|
||||
|
|
@ -63,7 +78,7 @@ export function profileMetaFields(dir, profileName) {
|
|||
const m = readProfileMeta(dir, profileName)
|
||||
// visibility is emitted ONLY when it is "public": absent means team, so every manifest in the
|
||||
// tree today keeps the bytes it has and nothing becomes visible by upgrading the generator.
|
||||
return { stage: m.stage, ...(m.visibility === 'public' ? { visibility: 'public' } : {}), ...(m.label ? { label: m.label } : {}), ...(m.targets ? { targets: m.targets } : {}), ...(m.heavy ? { heavy: true } : {}), ...(m.widget ? { widget: m.widget } : {}) }
|
||||
return { stage: m.stage, ...(m.visibility === 'public' ? { visibility: 'public' } : {}), ...(m.label ? { label: m.label } : {}), ...(m.targets ? { targets: m.targets } : {}), ...(m.heavy ? { heavy: true } : {}), ...(m.widget ? { widget: m.widget } : {}), ...(m.mobile ? { mobile: m.mobile } : {}) }
|
||||
}
|
||||
|
||||
/** A bundle is production if ANY of its profiles says so. */
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* never interfere with each other.
|
||||
*/
|
||||
|
||||
import { computed, provide, inject, watchEffect, watch } from 'vue'
|
||||
import { computed, provide, inject, watchEffect, watch, ref, onBeforeUnmount, useHost } from 'vue'
|
||||
import { useSharedMapToken, publishMapToken, fetchMapToken } from './composables/useSharedMapToken.js'
|
||||
import { retryableAsyncComponent } from './composables/retryableAsyncComponent.js'
|
||||
import { SUPPORTED_LOCALES, RTL_LOCALES, setRuntimeLocale, localeFromUrl } from './i18n/index.js'
|
||||
|
|
@ -26,6 +26,7 @@ const QaCorner = __DOXA_QA__
|
|||
? retryableAsyncComponent(() => import('./components/qa/QaCorner.vue'))
|
||||
: null
|
||||
import PinHint from './components/hints/PinHint.vue'
|
||||
import LargerScreenNotice from './components/LargerScreenNotice.vue'
|
||||
import { useUIStore } from './stores/uiStore.js'
|
||||
import { useMapStore } from './stores/mapStore.js'
|
||||
import { useDataStore } from './stores/dataStore.js'
|
||||
|
|
@ -207,6 +208,50 @@ const effectiveLocale = computed(() => lang.value || appI18n?.global?.locale?.va
|
|||
// the failure above, so it never touches the document at all.
|
||||
const dirAttr = computed(() => (RTL_LOCALES.has(effectiveLocale.value) ? 'rtl' : 'ltr'))
|
||||
|
||||
// ─── On a phone (SPEC-MOBILE-FLAG) ───────────────────────────
|
||||
// profile-config `mobile` comes from <profile>.meta.json through the leaf page (generate-tree):
|
||||
// 'desktop-only' on a narrow screen the profile is NOT mounted; a message says why
|
||||
// 'larger-screens' the profile mounts under a dismissible notice
|
||||
// Neither shows inside the Research Gallery, which keeps its own rail badge and pane notice: every
|
||||
// Gallery pane carries data-doxa-gallery, so a reader never gets two. (Not data-doxa-help: the Gallery
|
||||
// sets that only on panes whose map has help, so it would miss the rest.) The host is already in the
|
||||
// document when this runs, so that is known before anything mounts, and a blocked profile is never
|
||||
// mounted at all rather than mounted and then removed.
|
||||
const NARROW = '(max-width: 639.98px)'
|
||||
const mobileFlag = computed(() => {
|
||||
const m = config.value?.mobile
|
||||
return m === 'desktop-only' || m === 'larger-screens' ? m : null
|
||||
})
|
||||
let ceHost = null
|
||||
try { ceHost = useHost() } catch { ceHost = null }
|
||||
function insideGallery(node) {
|
||||
for (let n = node; n; n = n.parentNode || n.host) {
|
||||
if (n.nodeType === 1 && n.hasAttribute('data-doxa-gallery')) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
const hostedInGallery = ceHost ? insideGallery(ceHost) : false
|
||||
const narrow = ref(false)
|
||||
if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
|
||||
const mq = window.matchMedia(NARROW)
|
||||
narrow.value = mq.matches
|
||||
const onChange = (e) => { narrow.value = e.matches }
|
||||
mq.addEventListener?.('change', onChange)
|
||||
onBeforeUnmount(() => mq.removeEventListener?.('change', onChange))
|
||||
}
|
||||
const dismissKey = computed(() => `dx-maps:larger-screens-dismissed:${config.value?.profile || ''}`)
|
||||
const noticeDismissed = ref(false)
|
||||
watch(dismissKey, (k) => {
|
||||
try { noticeDismissed.value = sessionStorage.getItem(k) === '1' } catch { noticeDismissed.value = false }
|
||||
}, { immediate: true })
|
||||
function dismissLargerScreenNotice() {
|
||||
noticeDismissed.value = true
|
||||
try { sessionStorage.setItem(dismissKey.value, '1') } catch { /* private mode: dismissed until reload */ }
|
||||
}
|
||||
const blockedOnThisScreen = computed(() => mobileFlag.value === 'desktop-only' && narrow.value && !hostedInGallery)
|
||||
const adviseOnThisScreen = computed(() => mobileFlag.value === 'larger-screens' && narrow.value && !hostedInGallery && !noticeDismissed.value)
|
||||
const mobileLabel = computed(() => String(config.value?.mobileLabel || config.value?.profile || ''))
|
||||
|
||||
// ─── Instance ID ─────────────────────────────────────────────────────────────
|
||||
// Optional in profile-config: { "instanceId": "my-map" }
|
||||
// If not provided, a random ID is generated (stable for the element's lifetime).
|
||||
|
|
@ -282,7 +327,7 @@ provide('dataStore', dataStore)
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="profile-loader" :dir="dirAttr">
|
||||
<div class="profile-loader" :class="{ 'has-screen-notice': adviseOnThisScreen }" :dir="dirAttr">
|
||||
<!-- Error: no profile-config prop -->
|
||||
<div v-if="!config" class="profile-error">
|
||||
<p>⚠️ <strong>doxa-map</strong>: missing or invalid <code>profile-config</code> prop.</p>
|
||||
|
|
@ -294,8 +339,14 @@ provide('dataStore', dataStore)
|
|||
<p>⚠️ Profile <strong>"{{ profileName }}"</strong> not found.</p>
|
||||
</div>
|
||||
|
||||
<!-- Render the matched profile -->
|
||||
<component :is="ProfileComponent" v-else />
|
||||
<!-- A desktop-only map on a narrow screen: the message, and the profile is never mounted -->
|
||||
<LargerScreenNotice v-else-if="blockedOnThisScreen" mode="desktop-only" :label="mobileLabel" :locale="effectiveLocale" />
|
||||
|
||||
<!-- Render the matched profile, under the larger-screens notice when it applies -->
|
||||
<template v-else>
|
||||
<LargerScreenNotice v-if="adviseOnThisScreen" mode="larger-screens" :locale="effectiveLocale" @dismiss="dismissLargerScreenNotice" />
|
||||
<component :is="ProfileComponent" />
|
||||
</template>
|
||||
|
||||
<!-- QA corner widget (opt-in via profile-config.qa) — floats over any profile -->
|
||||
<component :is="QaCorner" v-if="ProfileComponent && showQa" />
|
||||
|
|
@ -336,6 +387,17 @@ provide('dataStore', dataStore)
|
|||
min-height: var(--profile-min-h, 100dvh);
|
||||
position: relative;
|
||||
}
|
||||
/* THE LARGER-SCREENS NOTICE SITS IN FLOW, so the plugin below it must give up exactly its height:
|
||||
a column, the notice at its natural size, the profile taking the rest (min-height:0 lets it shrink
|
||||
instead of overflowing the host by the notice's height). Only while the notice shows. */
|
||||
.profile-loader.has-screen-notice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.profile-loader.has-screen-notice > :not(.lsn-bar) {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.profile-error {
|
||||
padding: 1rem;
|
||||
|
|
|
|||
94
library/components/LargerScreenNotice.vue
Normal file
94
library/components/LargerScreenNotice.vue
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<!--
|
||||
LargerScreenNotice — what a map says on a phone when its <profile>.meta.json declares `mobile`
|
||||
(SPEC-MOBILE-FLAG). ProfileLoader decides WHETHER it shows (narrow screen, not inside the Research
|
||||
Gallery); this component only draws the two forms:
|
||||
|
||||
mode="larger-screens" a slim bar IN FLOW at the top of the plugin: the map still opens below it.
|
||||
Not an overlay, so it can never cover the map's own controls. The × emits
|
||||
`dismiss`; ProfileLoader remembers that per profile for the session.
|
||||
mode="desktop-only" the whole plugin area: the map is NOT mounted, and the message says why.
|
||||
|
||||
ONE TEXT: messages.largerScreenNotice / messages.desktopOnlyNotice are the Research Gallery's own
|
||||
sentences, moved into the library catalogue so the Gallery and a standalone leaf say the same thing
|
||||
in all eleven locales. tStatic with the element's locale, not useI18n: this mounts inside elements
|
||||
that may install no i18n plugin (the same reason PinHint reads tStatic).
|
||||
-->
|
||||
<template>
|
||||
<div v-if="mode === 'desktop-only'" class="lsn-block" role="status">
|
||||
<p class="lsn-block__text">{{ blockParts[0] }}<bdi v-if="blockParts.length > 1">{{ label }}</bdi>{{ blockParts[1] || '' }}</p>
|
||||
</div>
|
||||
<div v-else class="lsn-bar" role="note">
|
||||
<p class="lsn-bar__text">{{ adviseText }}</p>
|
||||
<button type="button" class="lsn-bar__x" :aria-label="dismissText" :title="dismissText" @click="$emit('dismiss')">×</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
|
||||
const props = defineProps({
|
||||
mode: { type: String, required: true },
|
||||
// The map's name for the desktop-only sentence ("{label} only supports desktop …").
|
||||
label: { type: String, default: '' },
|
||||
// The locale in force for THIS element (ProfileLoader's effectiveLocale), so the notice follows a
|
||||
// host's language switch instead of the page's boot language.
|
||||
locale: { type: String, default: '' },
|
||||
})
|
||||
defineEmits(['dismiss'])
|
||||
|
||||
const T = (key, en) => tStatic(key, props.locale || undefined) || en
|
||||
|
||||
const adviseText = computed(() => T('messages.largerScreenNotice',
|
||||
'This experimental map is built for larger screens. It opens here, but parts of it may not fit or work well on a phone.'))
|
||||
const dismissText = computed(() => T('messages.noticeDismiss', 'Dismiss'))
|
||||
// Split on the placeholder rather than replacing it, so the name can sit in its own <bdi>: a Latin map
|
||||
// name inside an Arabic sentence must not take the sentence's direction.
|
||||
const blockParts = computed(() => T('messages.desktopOnlyNotice',
|
||||
'{label} only supports desktop — open this page on a desktop to use it.').split('{label}'))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.lsn-bar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
flex: none;
|
||||
margin: 0;
|
||||
padding: 8px 8px 8px 12px;
|
||||
background: #fff8e1;
|
||||
border-block-end: 1px solid #f0d68a;
|
||||
color: #4a3b00;
|
||||
font: 400 13px/1.4 system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
}
|
||||
.lsn-bar__text { flex: 1 1 auto; margin: 0; }
|
||||
/* 24px is the smallest tap target the fleet accepts; the glyph stays small inside it. */
|
||||
.lsn-bar__x {
|
||||
flex: none;
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: 600 18px/1 system-ui, sans-serif;
|
||||
cursor: pointer;
|
||||
}
|
||||
.lsn-bar__x:focus-visible { outline: 2px solid #1a73e8; outline-offset: 2px; }
|
||||
.lsn-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
padding: 24px;
|
||||
background: #f7f7f5;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
font: 400 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
}
|
||||
.lsn-block__text { max-width: 28em; margin: 0; }
|
||||
</style>
|
||||
|
|
@ -37,5 +37,8 @@
|
|||
"countryWallIntroA": "ملصق لكل دولة، جاهز للصلاة والطباعة والتعليق على الحائط — نصلّي لأجلهم ",
|
||||
"countryWallIntroB": ". رُتّبت المناطق والدول بحسب عدد الشعوب غير المبلَّغة فيها، لتتقدّم الحاجة الأكبر.",
|
||||
"countryWallSoon": "قريبًا: ستُنشأ قائمة صلاة لكل دولة، تسمّي كل شعب غير مبلَّغ فيها.",
|
||||
"countryWallHeading": "ملصقات الدول — قوائم صلاة وملصقات"
|
||||
"countryWallHeading": "ملصقات الدول — قوائم صلاة وملصقات",
|
||||
"largerScreenNotice": "هذه الخريطة التجريبية مُعدّة للشاشات الكبيرة. تُفتح هنا، لكن بعض أجزائها قد لا يتّسع أو لا يعمل جيدًا على الهاتف.",
|
||||
"desktopOnlyNotice": "{label} يعمل على الحاسوب فقط — افتح هذه الصفحة على حاسوب لاستخدامه.",
|
||||
"noticeDismiss": "إغلاق"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,8 @@
|
|||
"countryWallIntroA": "Ein Poster pro Land, bereit zum Beten, Drucken und Aufhängen — und für sie beten ",
|
||||
"countryWallIntroB": ". Regionen und Länder sind danach geordnet, wie viele unerreichte Volksgruppen sie umfassen, damit der größte Bedarf vorne steht.",
|
||||
"countryWallSoon": "Demnächst: Für jedes Land wird eine Gebetsliste erstellt, die jede unerreichte Volksgruppe darin mit Namen nennt.",
|
||||
"countryWallHeading": "Länderposter — Gebetslisten und Poster"
|
||||
"countryWallHeading": "Länderposter — Gebetslisten und Poster",
|
||||
"largerScreenNotice": "Diese experimentelle Karte ist für große Bildschirme gebaut. Sie öffnet sich hier, aber Teile passen oder funktionieren auf einem Telefon womöglich nicht gut.",
|
||||
"desktopOnlyNotice": "{label} funktioniert nur am Desktop – öffnen Sie diese Seite an einem Computer.",
|
||||
"noticeDismiss": "Schließen"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,8 @@
|
|||
"countryWallIntroA": "One poster per country, ready to pray, print, and put on a wall — praying for them ",
|
||||
"countryWallIntroB": ". Regions and countries are ordered by how many unengaged people groups they hold, so the greatest need leads.",
|
||||
"countryWallSoon": "Coming soon — a prayer list will be generated for each country, naming every unengaged people group within it.",
|
||||
"countryWallHeading": "Country Posters — Prayer Lists & Posters"
|
||||
"countryWallHeading": "Country Posters — Prayer Lists & Posters",
|
||||
"largerScreenNotice": "This experimental map is built for larger screens. It opens here, but parts of it may not fit or work well on a phone.",
|
||||
"desktopOnlyNotice": "{label} only supports desktop — open this page on a desktop to use it.",
|
||||
"noticeDismiss": "Dismiss"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,8 @@
|
|||
"countryWallIntroA": "Un cartel por país, listo para orar, imprimir y colgar en una pared — orando por ellos ",
|
||||
"countryWallIntroB": ". Las regiones y los países se ordenan según cuántos pueblos no comprometidos contienen, para que la mayor necesidad vaya primero.",
|
||||
"countryWallSoon": "Próximamente: se generará una lista de oración para cada país, con el nombre de cada pueblo no alcanzado que hay en él.",
|
||||
"countryWallHeading": "Carteles por país — Listas de oración y carteles"
|
||||
"countryWallHeading": "Carteles por país — Listas de oración y carteles",
|
||||
"largerScreenNotice": "Este mapa experimental está pensado para pantallas grandes. Se abre aquí, pero algunas partes pueden no caber o no funcionar bien en un teléfono.",
|
||||
"desktopOnlyNotice": "{label} solo funciona en escritorio: abra esta página en un ordenador para usarlo.",
|
||||
"noticeDismiss": "Cerrar"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,8 @@
|
|||
"countryWallIntroA": "Une affiche par pays, prête à prier, à imprimer et à accrocher au mur — en priant pour eux ",
|
||||
"countryWallIntroB": ". Les régions et les pays sont classés selon le nombre de peuples non engagés qu’ils comptent, pour que le plus grand besoin vienne en premier.",
|
||||
"countryWallSoon": "Bientôt : une liste de prière sera générée pour chaque pays, nommant chaque peuple non atteint qui s’y trouve.",
|
||||
"countryWallHeading": "Affiches par pays — Listes de prière et affiches"
|
||||
"countryWallHeading": "Affiches par pays — Listes de prière et affiches",
|
||||
"largerScreenNotice": "Cette carte expérimentale est conçue pour les grands écrans. Elle s’ouvre ici, mais certaines parties peuvent mal s’afficher ou mal fonctionner sur un téléphone.",
|
||||
"desktopOnlyNotice": "{label} ne fonctionne que sur ordinateur : ouvrez cette page sur un ordinateur pour l’utiliser.",
|
||||
"noticeDismiss": "Fermer"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,8 @@
|
|||
"countryWallIntroA": "हर देश के लिए एक पोस्टर — प्रार्थना करने, छापने और दीवार पर लगाने के लिए तैयार, और उनके लिए प्रार्थना करते हुए ",
|
||||
"countryWallIntroB": ". क्षेत्र और देश इस क्रम में हैं कि उनमें कितने अनपहुँचे जनसमूह हैं, ताकि सबसे बड़ी ज़रूरत पहले आए।",
|
||||
"countryWallSoon": "जल्द ही: हर देश के लिए एक प्रार्थना सूची बनाई जाएगी, जिसमें उसके हर अनपहुँचे जनसमूह का नाम होगा।",
|
||||
"countryWallHeading": "देश पोस्टर — प्रार्थना सूचियाँ और पोस्टर"
|
||||
"countryWallHeading": "देश पोस्टर — प्रार्थना सूचियाँ और पोस्टर",
|
||||
"largerScreenNotice": "यह प्रायोगिक नक्शा बड़ी स्क्रीन के लिए बना है। यह यहाँ खुलता है, पर फ़ोन पर इसके कुछ हिस्से ठीक से न दिखें या न चलें।",
|
||||
"desktopOnlyNotice": "{label} केवल डेस्कटॉप पर चलता है — इसे इस्तेमाल करने के लिए यह पेज कंप्यूटर पर खोलें।",
|
||||
"noticeDismiss": "बंद करें"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,8 @@
|
|||
"countryWallIntroA": "Un poster per paese, pronto da pregare, stampare e appendere al muro — pregando per loro ",
|
||||
"countryWallIntroB": ". Regioni e paesi sono ordinati in base a quanti popoli non raggiunti contengono, perché il bisogno più grande venga per primo.",
|
||||
"countryWallSoon": "Prossimamente: per ogni paese verrà generata una lista di preghiera con il nome di ogni popolo non raggiunto che vi abita.",
|
||||
"countryWallHeading": "Poster per paese — Liste di preghiera e manifesti"
|
||||
"countryWallHeading": "Poster per paese — Liste di preghiera e manifesti",
|
||||
"largerScreenNotice": "Questa mappa sperimentale è pensata per schermi grandi. Si apre qui, ma alcune parti potrebbero non entrare o non funzionare bene su un telefono.",
|
||||
"desktopOnlyNotice": "{label} funziona solo su desktop: apri questa pagina da un computer per usarlo.",
|
||||
"noticeDismiss": "Chiudi"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,8 @@
|
|||
"countryWallIntroA": "Um cartaz por país, pronto para orar, imprimir e pendurar na parede — orando por eles ",
|
||||
"countryWallIntroB": ". As regiões e os países são ordenados pelo número de povos não engajados que abrigam, para que a maior necessidade venha primeiro.",
|
||||
"countryWallSoon": "Em breve: será gerada uma lista de oração para cada país, com o nome de cada povo não alcançado nele.",
|
||||
"countryWallHeading": "Cartazes por país — Listas de oração e cartazes"
|
||||
"countryWallHeading": "Cartazes por país — Listas de oração e cartazes",
|
||||
"largerScreenNotice": "Este mapa experimental foi feito para telas maiores. Ele abre aqui, mas algumas partes podem não caber ou não funcionar bem no celular.",
|
||||
"desktopOnlyNotice": "{label} só funciona no computador — abra esta página num computador para usá-lo.",
|
||||
"noticeDismiss": "Fechar"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,8 @@
|
|||
"countryWallIntroA": "Câte un afiș pentru fiecare țară, gata de rugăciune, de tipărit și de pus pe perete — rugându-ne pentru ei ",
|
||||
"countryWallIntroB": ". Regiunile și țările sunt ordonate după câte popoare neatinse cuprind, pentru ca nevoia cea mai mare să fie prima.",
|
||||
"countryWallSoon": "În curând: pentru fiecare țară va fi generată o listă de rugăciune care numește fiecare popor neatins din ea.",
|
||||
"countryWallHeading": "Afișe pe țări — Liste de rugăciune și afișe"
|
||||
"countryWallHeading": "Afișe pe țări — Liste de rugăciune și afișe",
|
||||
"largerScreenNotice": "Această hartă experimentală este făcută pentru ecrane mari. Se deschide aici, dar unele părți s-ar putea să nu încapă sau să nu funcționeze bine pe telefon.",
|
||||
"desktopOnlyNotice": "{label} funcționează doar pe desktop — deschideți pagina pe un computer pentru a-l folosi.",
|
||||
"noticeDismiss": "Închide"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,8 @@
|
|||
"countryWallIntroA": "Один плакат на страну — чтобы молиться, распечатать и повесить на стену, молясь за них ",
|
||||
"countryWallIntroB": ". Регионы и страны упорядочены по числу неохваченных народов, чтобы самая большая нужда стояла первой.",
|
||||
"countryWallSoon": "Скоро: для каждой страны будет создан молитвенный список с названием каждого неохваченного народа в ней.",
|
||||
"countryWallHeading": "Плакаты по странам — молитвенные списки и плакаты"
|
||||
"countryWallHeading": "Плакаты по странам — молитвенные списки и плакаты",
|
||||
"largerScreenNotice": "Эта экспериментальная карта рассчитана на большие экраны. Она откроется, но на телефоне часть элементов может не поместиться или работать плохо.",
|
||||
"desktopOnlyNotice": "{label} работает только на компьютере — откройте эту страницу на компьютере.",
|
||||
"noticeDismiss": "Закрыть"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,8 @@
|
|||
"countryWallIntroA": "每个国家一张海报,可用来祷告、打印、贴在墙上——为他们祷告,",
|
||||
"countryWallIntroB": "。各区域和国家按其中未得之民族群的数量排序,让最大的需要排在最前。",
|
||||
"countryWallSoon": "即将推出:将为每个国家生成一份祷告名单,列出其中每一个未得之民族群的名字。",
|
||||
"countryWallHeading": "国家海报 — 祷告名单与海报"
|
||||
"countryWallHeading": "国家海报 — 祷告名单与海报",
|
||||
"largerScreenNotice": "这张实验性地图为大屏幕设计。它可以在这里打开,但部分内容在手机上可能显示不全或无法正常使用。",
|
||||
"desktopOnlyNotice": "{label} 仅支持桌面端——请在电脑上打开此页面使用。",
|
||||
"noticeDismiss": "关闭"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@
|
|||
"cap:sync": "NOT `bunx` — bunx is not on PATH in this environment (command not found, exit 127), which broke cap:sync -> apk:debug -> phone at step one. The CLI IS installed locally (@capacitor/cli 7.6.9); invoke the local bin directly. The rest of package.json already uses the spaced `bun x` form.",
|
||||
"capability-exemplars": "regenerate documentation/reference-capability-exemplars.md — the DERIVED answer to \"which bundle is the capability-N example?\", a question answered WRONGLY in three separate artifacts (a gate, the README, the /plugin-designer skill).",
|
||||
"check:a11y-button-names": "every VISIBLE button must have an accessible name (aria-label / aria-labelledby / visible text — a `title` is a TOOLTIP and does NOT count). Measures the RENDERED DOM through shadow roots: a source grep cannot answer this (132 `<button>` hits, most with visible text). Needs a browser, so NOT in build:unlocked and NOT in CI. ALL=1 sweeps every profile.",
|
||||
"check:mobile-gating": "the SPA device rules stay on their shelves: DESKTOP_ONLY ids are curated production tabs (blocked on a narrow screen, disabled + a message naming the map), MOBILE_NOTICE ids are Experimental bundles (they open on a narrow screen with a dismissible not-built-for-mobile notice), the predicates read their set + narrow, and all 11 locales carry the four strings. Headless; --self-test proves each rule can fail.",
|
||||
"check:mobile-gating": "one phone flag per map (SPEC-MOBILE-FLAG): every <profile>.meta.json `mobile` is desktop-only or larger-screens; every flagged profile's leaf page carries its flag (and no leaf carries one its meta dropped); the Research Gallery derives its rules from the manifest, holds no id set, and mounts nothing on a phone before the flags land; dashboard-plus-grid stays desktop-only; the three notice sentences exist in all 11 locales and the desktop-only one names the map; ProfileLoader blocks, advises, and stays quiet inside the Gallery. --self-test plants one bad case per rule.",
|
||||
"check:approx-pin-note": "people-group pins say they are localities, not towns or boundaries, in ONE shared wording (library/components/legends/ApproxPinNote.vue): the colocation index finds groups sharing a coordinate, the note names them only for a shared point, never prints a raw coordinate, and renders from each locale's catalogue. Headless.",
|
||||
"check:auto-engine": "BROWSER. The default engine is auto: on one map leaf, no token draws with MapLibre and makes zero api.mapbox.com requests, a token draws with Mapbox, and an explicit provider wins. Under the fleet browser policy it is a rega run, not part of gates:post.",
|
||||
"check:no-new-driver-references": "shipped text (library, plugins, internal, docs, skills, README) neither quotes nor cites the Driver: a line that refers to the Driver and is not in internal/fixtures/driver-references-baseline.json FAILS. Forward-only: the baseline, keyed by file and line text with counts (not line numbers), may only shrink as owners sweep their areas (--shrink tidies it; it never grows). Matches the role word only, not a GPU or device driver. Node only.",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"stage": "production",
|
||||
"mobile": "desktop-only",
|
||||
"_comment": "Capability-4 composite that BAKES the research-dashboard + people-groups-grid into ONE self-contained element (both components imported directly + compiled into a single bundle, NO runtime bundle injection — unlike stack-demo which composes via src). Rendered stacked via the shared StackLayout so you scroll through both. This is the 'two elements baked into one' element the was asked for.",
|
||||
"type": "nested-elements-mfe",
|
||||
"composes": ["research-dashboard", "people-groups-grid"],
|
||||
|
|
|
|||
|
|
@ -128,11 +128,11 @@
|
|||
v-for="(t, i) in tabs" :key="t.id" class="dms-pane"
|
||||
:ref="el => { if (el) paneEls[i] = el; else delete paneEls[i] }"
|
||||
v-show="i === activeIndex" :hidden="i !== activeIndex"
|
||||
:data-doxa-help="tabHasHelp(t) ? '' : null">
|
||||
:data-doxa-help="tabHasHelp(t) ? '' : null" data-doxa-gallery="">
|
||||
|
||||
<template v-if="!t.heavy || i === activeIndex">
|
||||
<div v-if="showsMobileNotice(t.id)" class="dms-mobile-notice" role="status">
|
||||
<span>{{ tr('mobileNotice') }}</span>
|
||||
<span>{{ largerScreenText }}</span>
|
||||
<button type="button" class="dms-mobile-notice-x" :aria-label="tr('dismiss')" @click="dismissMobileNotice(t.id)">×</button>
|
||||
</div>
|
||||
<div class="dms-slot" :ref="el => setSlot(i, el)"></div>
|
||||
|
|
@ -196,6 +196,7 @@ import VerticalTabsLayout from '@map/layouts/VerticalTabsLayout.vue'
|
|||
import BackToTopButton from '@map/components/BackToTopButton.vue'
|
||||
import { SWITCHER_LANGUAGES, loadLocale, saveLocale, getLanguage } from '@map/layouts/app-wrapper-material/languages.js'
|
||||
import { translate, LOCALES, hasKey } from './src/strings.js'
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
import { helpFor } from './src/experimentalHelp.js'
|
||||
import { loadScriptOnce, whenTagDefined } from '@map/utils/embedScript.js'
|
||||
|
||||
|
|
@ -309,15 +310,26 @@ const NAV_MODEL = [
|
|||
// - people-groups-grid / research-dashboard / big-picture: the PARTS that are already composed
|
||||
// into the ONE `dashboard-plus-grid` Big Picture tab — surfacing them separately is the exact
|
||||
// redundancy the Driver removed ("we don't need multiple dashboards").
|
||||
// ── DEVICE RULES (Driver, 2026-09-28 16:50Z: DRV1650-MOBILE-GATING) ─────────────────────────
|
||||
// PRODUCTION maps that are not mobile-ready are BLOCKED on a narrow screen: the row stays in the rail,
|
||||
// disabled with a "desktop only" badge, and a tap explains why instead of mounting (Big Picture was the
|
||||
// first, and still works exactly that way). EXPERIMENTAL maps not built for mobile still OPEN on a
|
||||
// narrow screen, with a "not built for mobile" badge and a dismissible notice in the pane: an
|
||||
// experiment should be reachable, and honest about where it works. Add an id here, not a width test
|
||||
// anywhere else; check:mobile-gating holds the two sets to their shelves.
|
||||
const DESKTOP_ONLY = new Set(['dashboard-plus-grid'])
|
||||
const MOBILE_NOTICE = new Set(['gospel-internet'])
|
||||
// ── DEVICE RULES (DRV1650-MOBILE-GATING, SPEC-MOBILE-FLAG) ──────────────────────────────────────
|
||||
// A map that is not phone-ready says so ONCE, in its own <profile>.meta.json: "mobile": "desktop-only"
|
||||
// or "larger-screens". generate-tree carries the flag into the manifest, and this shell reads it from
|
||||
// there (mobileFlagsFromManifest), so the Gallery and the map's standalone leaf page follow the same
|
||||
// declaration and no id list lives here. On a narrow screen:
|
||||
// desktop-only the row stays in the rail, disabled with a "desktop only" badge; a tap explains why
|
||||
// instead of mounting (Big Picture was the first, and still works exactly that way)
|
||||
// larger-screens the map OPENS, with a "not built for mobile" badge and a dismissible notice in the
|
||||
// pane: reachable, and honest about where it works
|
||||
// check:mobile-gating holds the vocabulary, the leaf pages and this shell to that one source.
|
||||
function mobileFlagsFromManifest(manifest) {
|
||||
const flags = {}
|
||||
const bundles = (manifest && Array.isArray(manifest.bundles)) ? manifest.bundles : []
|
||||
for (const b of bundles) {
|
||||
for (const p of (Array.isArray(b && b.profiles) ? b.profiles : [])) {
|
||||
if (p && (p.mobile === 'desktop-only' || p.mobile === 'larger-screens')) flags[b.bundle + '/' + p.profile] = p.mobile
|
||||
}
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
const NOT_EXPERIMENTAL = new Set([
|
||||
'doxa-maps-spa',
|
||||
|
|
@ -812,6 +824,8 @@ export default defineComponent({
|
|||
disabledNotice: '', _noticeTimer: null,
|
||||
helpOpen: false,
|
||||
mobileNoticeDismissed: {},
|
||||
// Phone flags from the manifest (mobileFlagsFromManifest); null until it lands. See select().
|
||||
mobileFlags: null,
|
||||
// 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().
|
||||
|
|
@ -848,6 +862,12 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
// The pane notice for a larger-screens map: the library's sentence, so the Gallery and the map's
|
||||
// own leaf page say the same thing (SPEC-MOBILE-FLAG).
|
||||
largerScreenText() {
|
||||
return tStatic('messages.largerScreenNotice', this.uiLocale)
|
||||
|| 'This experimental map is built for larger screens. It opens here, but parts of it may not fit or work well on a phone.'
|
||||
},
|
||||
// ★ THE SCROLLER IS THIS PANE, MEASURED, NOT VTL'S. rega at 390x844 on the live build:
|
||||
// section.dms-pane has clientHeight 712 and scrollHeight 12,373 with overflow-y:auto, while
|
||||
// the document does not scroll at all and there is NO .vtl-host element in the tree.
|
||||
|
|
@ -944,7 +964,7 @@ export default defineComponent({
|
|||
rows = rows.concat(dedupeRows(
|
||||
this.tabs.filter(t => t.discovered)
|
||||
.map(t => ({ key: t.id, label: this.tabLabel(t), sub: this.tabSub(t), icon: 'flask',
|
||||
badge: MOBILE_NOTICE.has(t.id) && this.narrow ? this.tr('mobileNotBuilt') : this.tr('lab') })),
|
||||
badge: this.narrow && this.mobileFlagOf(t.id) === 'larger-screens' ? this.tr('mobileNotBuilt') : this.tr('lab') })),
|
||||
seen))
|
||||
}
|
||||
return { key: g.key, label: g.i18n ? this.tr(g.i18n) : g.label, icon: g.icon, folder: !!g.folder, open: g.open, rows }
|
||||
|
|
@ -1114,17 +1134,20 @@ export default defineComponent({
|
|||
if (this._mql.addEventListener) this._mql.addEventListener('change', this._onMedia)
|
||||
else if (this._mql.addListener) this._mql.addListener(this._onMedia)
|
||||
},
|
||||
// THE ONE ROW THE DEVICE RULE DISABLES. Driver, verbatim: "it shouldn't change the tab order, it
|
||||
// should open you up to the 2nd tab by default on mobile and disable the 1st tab." Named as a
|
||||
// predicate (not a hardcoded width filter in navGroups) so the ONE place that knows "Big Picture"
|
||||
// means 'dashboard-plus-grid' is this method — navGroups, onSelect and watchWidth all call it
|
||||
// rather than each re-encoding the id + the width test.
|
||||
isDesktopOnly(id) {
|
||||
return DESKTOP_ONLY.has(id) && this.narrow
|
||||
// The phone flag of a tab's own bundle/profile, as the manifest states it (null until it lands).
|
||||
mobileFlagOf(id) {
|
||||
const t = this.tabs.find(x => x.id === id)
|
||||
return (t && this.mobileFlags) ? (this.mobileFlags[t.bundle + '/' + t.profile] || null) : null
|
||||
},
|
||||
// An experimental map that opens on a phone but warns first (MOBILE_NOTICE), until dismissed.
|
||||
// THE ROWS THE DEVICE RULE DISABLES. The tab order does not change on a phone: the default opens
|
||||
// the next usable row and the disabled one stays in place. Named as a predicate (not a width filter
|
||||
// in navGroups) so navGroups, onSelect and watchWidth all ask the same question.
|
||||
isDesktopOnly(id) {
|
||||
return this.narrow && this.mobileFlagOf(id) === 'desktop-only'
|
||||
},
|
||||
// A map that opens on a phone but warns first, until dismissed.
|
||||
showsMobileNotice(id) {
|
||||
return MOBILE_NOTICE.has(id) && this.narrow && !this.mobileNoticeDismissed[id]
|
||||
return this.narrow && this.mobileFlagOf(id) === 'larger-screens' && !this.mobileNoticeDismissed[id]
|
||||
},
|
||||
dismissMobileNotice(id) {
|
||||
this.mobileNoticeDismissed = { ...this.mobileNoticeDismissed, [id]: true }
|
||||
|
|
@ -1226,6 +1249,7 @@ export default defineComponent({
|
|||
// The build's published defaults for a foreign-page embed (apiBaseUrl + public token). Set the
|
||||
// page globals BEFORE select() below mounts anything — a nested map reads them at init/fetch.
|
||||
this.runtime = (m && m.runtime && typeof m.runtime === 'object') ? m.runtime : {}
|
||||
this.mobileFlags = mobileFlagsFromManifest(m)
|
||||
this.applyPageGlobals()
|
||||
const patch = resolveTabsFromManifest(m, TAB_DEFS)
|
||||
const activeId = this.activeKey
|
||||
|
|
@ -1254,7 +1278,7 @@ export default defineComponent({
|
|||
const w = want ? this.tabs.findIndex(t => t.id === want) : -1
|
||||
const i = this.tabs.findIndex(t => t.id === activeId)
|
||||
if (w >= 0 && !this.isDesktopOnly(want) && !this._honouredTab) this.select(w)
|
||||
else this.select(i >= 0 ? i : this.firstVisibleIndex())
|
||||
else this.select(i >= 0 && !this.isDesktopOnly(activeId) ? i : this.firstVisibleIndex())
|
||||
this._honouredTab = true
|
||||
},
|
||||
// The tab a HOST asked to open first: the `tab` attribute, then profile-config.tab, then ?tab= on
|
||||
|
|
@ -1299,7 +1323,8 @@ export default defineComponent({
|
|||
onSelect(key) {
|
||||
if (this.isDesktopOnly(key)) {
|
||||
const t = this.tabs.find(x => x.id === key)
|
||||
this.disabledNotice = this.tr('desktopNotice', { label: t ? this.tabLabel(t) : key })
|
||||
this.disabledNotice = (tStatic('messages.desktopOnlyNotice', this.uiLocale) || '{label} only supports desktop — open this page on a desktop to use it.')
|
||||
.replace('{label}', t ? this.tabLabel(t) : key)
|
||||
clearTimeout(this._noticeTimer)
|
||||
this._noticeTimer = setTimeout(() => { this.disabledNotice = '' }, 4000)
|
||||
return
|
||||
|
|
@ -1355,6 +1380,12 @@ export default defineComponent({
|
|||
if (prevIndex !== i || !this._announced) { this._announced = true; this.$emit('tab-change', { id: t.id, label: t.label }) }
|
||||
// Not yet completed by the manifest — applyManifest calls select() again once it lands.
|
||||
if (!t.scriptUrl) return
|
||||
// ★ ON A PHONE, NOTHING MOUNTS BEFORE THE FLAGS ARE KNOWN. They come with the manifest, and the
|
||||
// baked first tab (Big Picture) has its scriptUrl before that: mounting it here would put the
|
||||
// heaviest, desktop-only map on a phone for the length of one request. Fail closed; applyManifest
|
||||
// re-selects once the flags are in. On a desktop this changes nothing.
|
||||
if (this.narrow && this.mobileFlags === null) return
|
||||
if (this.isDesktopOnly(t.id)) return
|
||||
// A heavy tab's slot only exists after its v-if subtree renders, so wait a tick before mounting
|
||||
// into it: on the first activation the ref is still null at this point.
|
||||
if (!t.mounted) this.$nextTick(() => { if (this.activeIndex === i) this.mountElement(i) })
|
||||
|
|
@ -1605,7 +1636,7 @@ export default defineComponent({
|
|||
padding: 10px 16px; box-shadow: 0 8px 24px rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
/* "Not built for mobile" (MOBILE_NOTICE): sticky at the top of the pane's own scroller, so it stays in
|
||||
/* "Not built for mobile" (a "larger-screens" map): sticky at the top of the pane's own scroller, so it stays in
|
||||
view over the map and never covers the rail. Dismissible per map for the session. */
|
||||
.dms-mobile-notice {
|
||||
position: sticky; top: 0; z-index: 30; display: flex; align-items: flex-start; gap: 8px;
|
||||
|
|
|
|||
|
|
@ -29,13 +29,12 @@ export const STRINGS = {
|
|||
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.',
|
||||
close: 'Close', help: 'About this map', desktopOnly: 'desktop only', lab: 'lab', experimentalSub: 'experimental bundle',
|
||||
mobileNotBuilt: 'not built for mobile', mobileNotice: 'This experimental map is built for larger screens. It opens here, but parts of it may not fit or work well on a phone.', dismiss: 'Dismiss', desktopOnly: 'desktop only', lab: 'lab', experimentalSub: 'experimental bundle',
|
||||
mobileNotBuilt: 'not built for mobile', dismiss: 'Dismiss', 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",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Prayer Walk Planner", 'tab.upg-neighbor': "UPG Neighbor", 'tab.key-cities': "Key Cities", 'tab.all-people-groups': "All People Groups", 'tab.pplr-data-maps': "PPLR Data Maps", 'tab.wall-of-people-groups': "Wall of People Groups", 'tab.country-posters': "Country Posters", 'tab.doxa-vision-card': "Doxa Vision Card",
|
||||
desktopNotice: '{label} only supports desktop — open this page on a desktop to use it.'
|
||||
},
|
||||
es: {
|
||||
backToTop: 'Volver arriba',
|
||||
|
|
@ -51,13 +50,12 @@ export const STRINGS = {
|
|||
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.',
|
||||
close: 'Cerrar', help: 'Acerca de este mapa', desktopOnly: 'solo escritorio', lab: 'beta', experimentalSub: 'mapa experimental',
|
||||
mobileNotBuilt: 'no diseñado para móvil', mobileNotice: 'Este mapa experimental está pensado para pantallas grandes. Se abre aquí, pero algunas partes pueden no caber o no funcionar bien en un teléfono.', dismiss: 'Cerrar', desktopOnly: 'solo escritorio', lab: 'beta', experimentalSub: 'mapa experimental',
|
||||
mobileNotBuilt: 'no diseñado para móvil', dismiss: 'Cerrar', 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",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Caminatas de oración", 'tab.upg-neighbor': "Vecino UPG", 'tab.key-cities': "Ciudades clave", 'tab.all-people-groups': "Todos los pueblos", 'tab.pplr-data-maps': "Mapas de datos PPLR", 'tab.wall-of-people-groups': "Muro de los pueblos", 'tab.country-posters': "Carteles por país", 'tab.doxa-vision-card': "Tarjeta de visión Doxa",
|
||||
desktopNotice: '{label} solo funciona en escritorio: abra esta página en un ordenador para usarlo.'
|
||||
},
|
||||
fr: {
|
||||
backToTop: 'Retour en haut',
|
||||
|
|
@ -73,13 +71,12 @@ export const STRINGS = {
|
|||
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.",
|
||||
close: 'Fermer', help: 'À propos de cette carte', desktopOnly: 'ordinateur uniquement', lab: 'bêta', experimentalSub: 'carte expérimentale',
|
||||
mobileNotBuilt: 'pas conçu pour mobile', mobileNotice: 'Cette carte expérimentale est conçue pour les grands écrans. Elle s’ouvre ici, mais certaines parties peuvent mal s’afficher ou mal fonctionner sur un téléphone.', dismiss: 'Fermer', desktopOnly: 'ordinateur uniquement', lab: 'bêta', experimentalSub: 'carte expérimentale',
|
||||
mobileNotBuilt: 'pas conçu pour mobile', dismiss: 'Fermer', 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",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Marches de prière", 'tab.upg-neighbor': "Voisin UPG", 'tab.key-cities': "Villes clés", 'tab.all-people-groups': "Tous les peuples", 'tab.pplr-data-maps': "Cartes de données PPLR", 'tab.wall-of-people-groups': "Mur des peuples", 'tab.country-posters': "Affiches par pays", 'tab.doxa-vision-card': "Carte de vision Doxa",
|
||||
desktopNotice: '{label} ne fonctionne que sur ordinateur : ouvrez cette page sur un ordinateur pour l’utiliser.'
|
||||
},
|
||||
de: {
|
||||
backToTop: 'Nach oben',
|
||||
|
|
@ -95,13 +92,12 @@ export const STRINGS = {
|
|||
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.',
|
||||
close: 'Schließen', help: 'Über diese Karte', desktopOnly: 'nur Desktop', lab: 'Beta', experimentalSub: 'experimentelle Karte',
|
||||
mobileNotBuilt: 'nicht für Mobilgeräte', mobileNotice: 'Diese experimentelle Karte ist für große Bildschirme gebaut. Sie öffnet sich hier, aber Teile passen oder funktionieren auf einem Telefon womöglich nicht gut.', dismiss: 'Schließen', desktopOnly: 'nur Desktop', lab: 'Beta', experimentalSub: 'experimentelle Karte',
|
||||
mobileNotBuilt: 'nicht für Mobilgeräte', dismiss: 'Schließen', 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",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Gebetsspaziergänge", 'tab.upg-neighbor': "UPG-Nachbar", 'tab.key-cities': "Schlüsselstädte", 'tab.all-people-groups': "Alle Volksgruppen", 'tab.pplr-data-maps': "PPLR-Datenkarten", 'tab.wall-of-people-groups': "Wand der Volksgruppen", 'tab.country-posters': "Länderposter", 'tab.doxa-vision-card': "Doxa-Visionskarte",
|
||||
desktopNotice: '{label} funktioniert nur am Desktop – öffnen Sie diese Seite an einem Computer.'
|
||||
},
|
||||
pt: {
|
||||
backToTop: 'Voltar ao topo',
|
||||
|
|
@ -117,13 +113,12 @@ export const STRINGS = {
|
|||
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.',
|
||||
close: 'Fechar', help: 'Sobre este mapa', desktopOnly: 'só no computador', lab: 'beta', experimentalSub: 'mapa experimental',
|
||||
mobileNotBuilt: 'não feito para celular', mobileNotice: 'Este mapa experimental foi feito para telas maiores. Ele abre aqui, mas algumas partes podem não caber ou não funcionar bem no celular.', dismiss: 'Fechar', desktopOnly: 'só no computador', lab: 'beta', experimentalSub: 'mapa experimental',
|
||||
mobileNotBuilt: 'não feito para celular', dismiss: 'Fechar', 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",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Caminhadas de oração", 'tab.upg-neighbor': "Vizinho UPG", 'tab.key-cities': "Cidades-chave", 'tab.all-people-groups': "Todos os povos", 'tab.pplr-data-maps': "Mapas de dados PPLR", 'tab.wall-of-people-groups': "Mural dos povos", 'tab.country-posters': "Cartazes por país", 'tab.doxa-vision-card': "Cartão de visão Doxa",
|
||||
desktopNotice: '{label} só funciona no computador — abra esta página num computador para usá-lo.'
|
||||
},
|
||||
it: {
|
||||
backToTop: 'Torna su',
|
||||
|
|
@ -141,13 +136,12 @@ export const STRINGS = {
|
|||
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.",
|
||||
close: 'Chiudi', help: 'Informazioni su questa mappa', desktopOnly: 'solo desktop', lab: 'beta', experimentalSub: 'mappa sperimentale',
|
||||
mobileNotBuilt: 'non pensato per mobile', mobileNotice: 'Questa mappa sperimentale è pensata per schermi grandi. Si apre qui, ma alcune parti potrebbero non entrare o non funzionare bene su un telefono.', dismiss: 'Chiudi', desktopOnly: 'solo desktop', lab: 'beta', experimentalSub: 'mappa sperimentale',
|
||||
mobileNotBuilt: 'non pensato per mobile', dismiss: 'Chiudi', 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",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Passeggiate di preghiera", 'tab.upg-neighbor': "Vicino UPG", 'tab.key-cities': "Città chiave", 'tab.all-people-groups': "Tutti i popoli", 'tab.pplr-data-maps': "Mappe dati PPLR", 'tab.wall-of-people-groups': "Muro dei popoli", 'tab.country-posters': "Poster per paese", 'tab.doxa-vision-card': "Scheda visione Doxa",
|
||||
desktopNotice: '{label} funziona solo su desktop: apri questa pagina da un computer per usarlo.'
|
||||
},
|
||||
ro: {
|
||||
backToTop: 'Sus',
|
||||
|
|
@ -163,13 +157,12 @@ export const STRINGS = {
|
|||
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.',
|
||||
close: 'Închide', help: 'Despre această hartă', desktopOnly: 'doar pe desktop', lab: 'beta', experimentalSub: 'hartă experimentală',
|
||||
mobileNotBuilt: 'nu e făcut pentru mobil', mobileNotice: 'Această hartă experimentală este făcută pentru ecrane mari. Se deschide aici, dar unele părți s-ar putea să nu încapă sau să nu funcționeze bine pe telefon.', dismiss: 'Închide', desktopOnly: 'doar pe desktop', lab: 'beta', experimentalSub: 'hartă experimentală',
|
||||
mobileNotBuilt: 'nu e făcut pentru mobil', dismiss: 'Închide', 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",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Plimbări de rugăciune", 'tab.upg-neighbor': "Vecin UPG", 'tab.key-cities': "Orașe-cheie", 'tab.all-people-groups': "Toate popoarele", 'tab.pplr-data-maps': "Hărți de date PPLR", 'tab.wall-of-people-groups': "Zidul popoarelor", 'tab.country-posters': "Afișe pe țări", 'tab.doxa-vision-card': "Card de viziune Doxa",
|
||||
desktopNotice: '{label} funcționează doar pe desktop — deschideți pagina pe un computer pentru a-l folosi.'
|
||||
},
|
||||
ru: {
|
||||
backToTop: 'Наверх',
|
||||
|
|
@ -185,13 +178,12 @@ export const STRINGS = {
|
|||
navLabel: 'Исследовательские карты', meta: 'Карт: {n}', loading: 'Загрузка: {label}…', loadFailed: 'Не удалось загрузить: {label}.',
|
||||
manifestHint: 'Эта вкладка собирается из manifest.json — соберите дерево или разместите собранное дерево, чтобы загрузить её.',
|
||||
close: 'Закрыть', help: 'Об этой карте', desktopOnly: 'только на компьютере', lab: 'бета', experimentalSub: 'экспериментальная карта',
|
||||
mobileNotBuilt: 'не для телефона', mobileNotice: 'Эта экспериментальная карта рассчитана на большие экраны. Она откроется, но на телефоне часть элементов может не поместиться или работать плохо.', dismiss: 'Закрыть', desktopOnly: 'только на компьютере', lab: 'бета', experimentalSub: 'экспериментальная карта',
|
||||
mobileNotBuilt: 'не для телефона', dismiss: 'Закрыть', 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: "Свернуть",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "Молитвенные прогулки", 'tab.upg-neighbor': "Сосед UPG", 'tab.key-cities': "Ключевые города", 'tab.all-people-groups': "Все народы", 'tab.pplr-data-maps': "Карты данных PPLR", 'tab.wall-of-people-groups': "Стена народов", 'tab.country-posters': "Плакаты по странам", 'tab.doxa-vision-card': "Карточка видения Doxa",
|
||||
desktopNotice: '{label} работает только на компьютере — откройте эту страницу на компьютере.'
|
||||
},
|
||||
ar: {
|
||||
backToTop: 'إلى الأعلى',
|
||||
|
|
@ -207,13 +199,12 @@ export const STRINGS = {
|
|||
navLabel: 'خرائط البحث', meta: 'عدد الخرائط: {n}', loading: 'جارٍ تحميل {label}…', loadFailed: 'تعذّر تحميل {label}.',
|
||||
manifestHint: 'تُبنى علامة التبويب هذه من manifest.json — ابنِ الشجرة أو قدّم الشجرة المبنية لتحميلها.',
|
||||
close: 'إغلاق', help: 'عن هذه الخريطة', desktopOnly: 'على الحاسوب فقط', lab: 'تجريبي', experimentalSub: 'خريطة تجريبية',
|
||||
mobileNotBuilt: 'غير مُعدّ للهاتف', mobileNotice: 'هذه الخريطة التجريبية مُعدّة للشاشات الكبيرة. تُفتح هنا، لكن بعض أجزائها قد لا يتّسع أو لا يعمل جيدًا على الهاتف.', dismiss: 'إغلاق', desktopOnly: 'على الحاسوب فقط', lab: 'تجريبي', experimentalSub: 'خريطة تجريبية',
|
||||
mobileNotBuilt: 'غير مُعدّ للهاتف', dismiss: 'إغلاق', 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: "طي",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "جولات الصلاة", 'tab.upg-neighbor': "جار UPG", 'tab.key-cities': "المدن الرئيسية", 'tab.all-people-groups': "جميع الشعوب", 'tab.pplr-data-maps': "خرائط بيانات PPLR", 'tab.wall-of-people-groups': "جدار الشعوب", 'tab.country-posters': "ملصقات الدول", 'tab.doxa-vision-card': "بطاقة رؤية Doxa",
|
||||
desktopNotice: '{label} يعمل على الحاسوب فقط — افتح هذه الصفحة على حاسوب لاستخدامه.'
|
||||
},
|
||||
hi: {
|
||||
backToTop: 'ऊपर जाएँ',
|
||||
|
|
@ -229,13 +220,12 @@ export const STRINGS = {
|
|||
navLabel: 'शोध मानचित्र', meta: '{n} मानचित्र', loading: '{label} लोड हो रहा है…', loadFailed: '{label} लोड नहीं हो सका।',
|
||||
manifestHint: 'यह टैब manifest.json से बनता है — इसे लोड करने के लिए ट्री बनाएँ या बना हुआ ट्री सर्व करें।',
|
||||
close: 'बंद करें', help: 'इस नक्शे के बारे में', desktopOnly: 'केवल डेस्कटॉप', lab: 'बीटा', experimentalSub: 'प्रायोगिक मानचित्र',
|
||||
mobileNotBuilt: 'मोबाइल के लिए नहीं बना', mobileNotice: 'यह प्रायोगिक नक्शा बड़ी स्क्रीन के लिए बना है। यह यहाँ खुलता है, पर फ़ोन पर इसके कुछ हिस्से ठीक से न दिखें या न चलें।', dismiss: 'बंद करें', desktopOnly: 'केवल डेस्कटॉप', lab: 'बीटा', experimentalSub: 'प्रायोगिक मानचित्र',
|
||||
mobileNotBuilt: 'मोबाइल के लिए नहीं बना', dismiss: 'बंद करें', 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: "संक्षिप्त करें",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "प्रार्थना पदयात्रा", 'tab.upg-neighbor': "UPG पड़ोसी", 'tab.key-cities': "प्रमुख शहर", 'tab.all-people-groups': "सभी जनसमूह", 'tab.pplr-data-maps': "PPLR डेटा मानचित्र", 'tab.wall-of-people-groups': "जनसमूहों की दीवार", 'tab.country-posters': "देश पोस्टर", 'tab.doxa-vision-card': "Doxa दर्शन कार्ड",
|
||||
desktopNotice: '{label} केवल डेस्कटॉप पर चलता है — इसे इस्तेमाल करने के लिए यह पेज कंप्यूटर पर खोलें।'
|
||||
},
|
||||
zh: {
|
||||
backToTop: '回到顶部',
|
||||
|
|
@ -251,13 +241,12 @@ export const STRINGS = {
|
|||
navLabel: '研究地图', meta: '{n} 张地图', loading: '正在加载{label}…', loadFailed: '无法加载{label}。',
|
||||
manifestHint: '此标签页由 manifest.json 组成——请构建目录树或提供已构建的目录树以加载它。',
|
||||
close: '关闭', help: '关于此地图', desktopOnly: '仅限桌面', lab: '测试版', experimentalSub: '实验性地图',
|
||||
mobileNotBuilt: '未针对手机设计', mobileNotice: '这张实验性地图为大屏幕设计。它可以在这里打开,但部分内容在手机上可能显示不全或无法正常使用。', dismiss: '关闭', desktopOnly: '仅限桌面', lab: '测试版', experimentalSub: '实验性地图',
|
||||
mobileNotBuilt: '未针对手机设计', dismiss: '关闭', 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: "折叠",
|
||||
// Experimental bundles known today (rail-length names; a bundle not listed keeps its manifest title).
|
||||
'tab.prayer-walk-planner': "祷告行走", 'tab.upg-neighbor': "UPG 邻居", 'tab.key-cities': "重点城市", 'tab.all-people-groups': "所有民族", 'tab.pplr-data-maps': "PPLR 数据地图", 'tab.wall-of-people-groups': "民族之墙", 'tab.country-posters': "国家海报", 'tab.doxa-vision-card': "Doxa 异象卡",
|
||||
desktopNotice: '{label} 仅支持桌面端——请在电脑上打开此页面使用。'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,5 +7,6 @@
|
|||
},
|
||||
"note": "pinHint:false because the board has its own legend and drawer; the shared coach-mark would float over the facet rail. urlState:'query' makes the board's own page keep ?view=&access=&band=&country=&province=&language=&group= in the address bar (shareable views, README.md, \"The views\"). A partner embedding the element without it keeps their URL untouched: the board READS the query always and WRITES it only when asked.",
|
||||
"stage": "experimental",
|
||||
"mobile": "larger-screens",
|
||||
"heavy": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
(function () {
|
||||
var slot = document.getElementById('slot');
|
||||
var status = document.getElementById('status');
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","profile":"dashboard-plus-grid"};
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","mobile":"desktop-only","mobileLabel":"Dashboard + Grid","profile":"dashboard-plus-grid"};
|
||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||
// the ladder below is skipped entirely and the element mounts immediately.
|
||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
(function () {
|
||||
var slot = document.getElementById('slot');
|
||||
var status = document.getElementById('status');
|
||||
var BASE_CONFIG = {"profile":"dashboard-plus-grid","dataSource":"pray-tools","instanceId":"dashboard-plus-grid-dashboard-plus-grid","assetsBase":"../../assets"};
|
||||
var BASE_CONFIG = {"mobile":"desktop-only","mobileLabel":"Dashboard + Grid","profile":"dashboard-plus-grid","dataSource":"pray-tools","instanceId":"dashboard-plus-grid-dashboard-plus-grid","assetsBase":"../../assets"};
|
||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||
// the ladder below is skipped entirely and the element mounts immediately.
|
||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
||||
|
|
|
|||
|
|
@ -275,6 +275,7 @@
|
|||
"profile": "dashboard-plus-grid",
|
||||
"stage": "production",
|
||||
"label": "Dashboard + Grid (baked into one element, stacked to scroll)",
|
||||
"mobile": "desktop-only",
|
||||
"file": "dashboard-plus-grid.vue",
|
||||
"description": "Both children are IMPORTED DIRECTLY (compiled into this ONE bundle — no runtime bundle",
|
||||
"element": "dashboard-plus-grid",
|
||||
|
|
@ -654,6 +655,7 @@
|
|||
"stage": "experimental",
|
||||
"label": "Gospel × Internet",
|
||||
"heavy": true,
|
||||
"mobile": "larger-screens",
|
||||
"file": "board.vue",
|
||||
"description": "For every remaining people group: what gospel resources exist in a language they use, and how much of their country (or Indian state) is online to receive them.",
|
||||
"element": "gospel-internet-board",
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@
|
|||
(function () {
|
||||
var slot = document.getElementById('slot');
|
||||
var status = document.getElementById('status');
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","pinHint":false,"urlState":"query","profile":"board"};
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","pinHint":false,"urlState":"query","mobile":"larger-screens","mobileLabel":"Gospel × Internet","profile":"board"};
|
||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||
// the ladder below is skipped entirely and the element mounts immediately.
|
||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@
|
|||
(function () {
|
||||
var slot = document.getElementById('slot');
|
||||
var status = document.getElementById('status');
|
||||
var BASE_CONFIG = {"pinHint":false,"urlState":"query","profile":"board","dataSource":"pray-tools","instanceId":"gospel-internet-board","assetsBase":"../../assets"};
|
||||
var BASE_CONFIG = {"pinHint":false,"urlState":"query","mobile":"larger-screens","mobileLabel":"Gospel × Internet","profile":"board","dataSource":"pray-tools","instanceId":"gospel-internet-board","assetsBase":"../../assets"};
|
||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||
// the ladder below is skipped entirely and the element mounts immediately.
|
||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@
|
|||
(function () {
|
||||
var slot = document.getElementById('slot');
|
||||
var status = document.getElementById('status');
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","pinHint":false,"urlState":"query","profile":"board","view":"province","panel":"map"};
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","pinHint":false,"urlState":"query","mobile":"larger-screens","mobileLabel":"Gospel × Internet","profile":"board","view":"province","panel":"map"};
|
||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||
// the ladder below is skipped entirely and the element mounts immediately.
|
||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@
|
|||
(function () {
|
||||
var slot = document.getElementById('slot');
|
||||
var status = document.getElementById('status');
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","pinHint":false,"urlState":"query","profile":"board","panel":"tree"};
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","pinHint":false,"urlState":"query","mobile":"larger-screens","mobileLabel":"Gospel × Internet","profile":"board","panel":"tree"};
|
||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||
// the ladder below is skipped entirely and the element mounts immediately.
|
||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@
|
|||
(function () {
|
||||
var slot = document.getElementById('slot');
|
||||
var status = document.getElementById('status');
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","pinHint":false,"urlState":"query","profile":"board","view":"platforms"};
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","pinHint":false,"urlState":"query","mobile":"larger-screens","mobileLabel":"Gospel × Internet","profile":"board","view":"platforms"};
|
||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||
// the ladder below is skipped entirely and the element mounts immediately.
|
||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@
|
|||
(function () {
|
||||
var slot = document.getElementById('slot');
|
||||
var status = document.getElementById('status');
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","pinHint":false,"urlState":"query","profile":"board","list":"700"};
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","pinHint":false,"urlState":"query","mobile":"larger-screens","mobileLabel":"Gospel × Internet","profile":"board","list":"700"};
|
||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||
// the ladder below is skipped entirely and the element mounts immediately.
|
||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@
|
|||
(function () {
|
||||
var slot = document.getElementById('slot');
|
||||
var status = document.getElementById('status');
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","pinHint":false,"urlState":"query","profile":"board","view":"zero"};
|
||||
var BASE_CONFIG = {"assetsBase":"../../../assets","pinHint":false,"urlState":"query","mobile":"larger-screens","mobileLabel":"Gospel × Internet","profile":"board","view":"zero"};
|
||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||
// the ladder below is skipped entirely and the element mounts immediately.
|
||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
||||
|
|
|
|||
|
|
@ -275,6 +275,7 @@
|
|||
"profile": "dashboard-plus-grid",
|
||||
"stage": "production",
|
||||
"label": "Dashboard + Grid (baked into one element, stacked to scroll)",
|
||||
"mobile": "desktop-only",
|
||||
"file": "dashboard-plus-grid.vue",
|
||||
"description": "Both children are IMPORTED DIRECTLY (compiled into this ONE bundle — no runtime bundle",
|
||||
"element": "dashboard-plus-grid",
|
||||
|
|
@ -654,6 +655,7 @@
|
|||
"stage": "experimental",
|
||||
"label": "Gospel × Internet",
|
||||
"heavy": true,
|
||||
"mobile": "larger-screens",
|
||||
"file": "board.vue",
|
||||
"description": "For every remaining people group: what gospel resources exist in a language they use, and how much of their country (or Indian state) is online to receive them.",
|
||||
"element": "gospel-internet-board",
|
||||
|
|
|
|||
Loading…
Reference in a new issue