diff --git a/internal/check-packet-browser.mjs b/internal/check-packet-browser.mjs index f0587495f..f408edd59 100644 --- a/internal/check-packet-browser.mjs +++ b/internal/check-packet-browser.mjs @@ -143,6 +143,7 @@ const r = await pg.evaluate(()=>({ // on a CORRECTLY translated packet (the same trap already fixed in // check-packet-render). mark5Len: (document.querySelector('.p4-passage')||{textContent:''}).textContent.trim().length, + heartItems: document.querySelectorAll('.p4-heart li').length, // ★ THE SAME PACKET, EXPORTED THROUGH A DIFFERENT PATH, WAS HELD TO A WEAKER STANDARD. // check-packet-render gained these over several ticks — the 12 points, the wheel's 12 // LABELS (not just its 12 segments), page 1's geography — each found by mutation, each @@ -200,7 +201,7 @@ console.log(` prayer points : ${r.points} / 12`) console.log(` wheel segments : ${r.wheel} (${r.wheelLabels}/12 labelled)`) console.log(` map (page 1) : ${r.mapGeoPaths} geography paths`) console.log(` printed artefact : ${(pdfBytes/1024/1024).toFixed(1)}MB, ${pdfFonts} embedded font(s)`) -console.log(` Mark 5 in full : ${r.mark5Len} chars`) // ⚠ mark5Len, not mark5 — the latter is not a key on r and printed "undefined" +console.log(` point 4's list : ${r.heartItems} readings`) // beside a PASS for as long as this gate has existed. The ASSERTION below always read // the right key, so the requirement was protected; only the display was blind. console.log(` metacamp.org : ${r.metacamp}`) @@ -252,11 +253,13 @@ if (pdfFonts === 0) { } if (r.metacamp !== 2) fails.push(`${r.metacamp} metacamp.org attributions, expected exactly 2`) // Mark 5:1-20 in full, by LENGTH rather than an English proper noun. -if (r.mark5Len < 1200) { - fails.push(r.mark5Len === 0 - ? 'Mark 5:1-20 is NOT printed (no .p4-passage block)' - : `Mark 5:1-20 looks truncated: ${r.mark5Len} chars`) +// Point 4 prints THE HEAVENLY FATHER'S HEART (seven readings) in place of Mark 5:1-20 in full (2026-09-30). +if (r.heartItems !== 7) { + fails.push(r.heartItems === 0 + ? "point 4's reading list is NOT printed (no .p4-heart block)" + : `point 4's reading list prints ${r.heartItems} of 7 readings`) } + // PAGE SIZE was printed and never compared. `@page{size:letter portrait}` is // 612x792pt; a wrong PORTRAIT size (A4 is 595x842) is taller-than-wide too, so // nothing else here would notice it. diff --git a/internal/check-packet-render.mjs b/internal/check-packet-render.mjs index 6c3bfce69..028d98a28 100644 --- a/internal/check-packet-render.mjs +++ b/internal/check-packet-render.mjs @@ -401,6 +401,10 @@ const r = await page.evaluate(() => { // bug were locked together. Assert the STRUCTURE instead — the passage block exists // and carries a full passage's worth of text — which holds in any language. mark5Len: (document.querySelector('.p4-passage')?.textContent || '').trim().length, + // ★ POINT 4 NOW PRINTS A READING LIST, NOT THE PASSAGE (2026-09-30): THE HEAVENLY FATHER'S HEART, seven + // readings. Counted by STRUCTURE (the .p4-heart hook and its items), which holds in any language. + heartItems: document.querySelectorAll('.p4-heart li').length, + heartTitle: (document.querySelector('.p4-heart-h')?.textContent || '').trim().length, // ⚠ COUNT THE POINTS. The sheet-sum invariant below already required a points PAGE — // and a page with three points on it is still exactly one sheet, so a packet headed // "12 Prayer Points" printed THREE and every number this gate asserted stayed correct. @@ -532,11 +536,13 @@ if (r.rhythms !== r.rhythmsTotal) fails.push(`only ${r.rhythms}/${r.rhythmsTotal // English passage is 2,141 chars; 1,200 is a floor that no truncation or placeholder // survives while leaving room for languages that render the same text more compactly // (zh is typically ~40% shorter in characters). -const MARK5_MIN = 1200 -if (r.mark5Len < MARK5_MIN) { - fails.push(r.mark5Len === 0 - ? 'Mark 5:1-20 is NOT printed in full (a requirement for point 4) — no .p4-passage block' - : `Mark 5:1-20 looks truncated: ${r.mark5Len} chars, expected at least ${MARK5_MIN}`) +// Point 4 prints THE HEAVENLY FATHER'S HEART: a heading and seven readings, each addressed in the reader's +// own Bible. It replaced Mark 5:1-20 in full (2026-09-30), so the passage is no longer asserted; the list is. +const HEART_ITEMS = 7 +if (r.heartItems !== HEART_ITEMS || !r.heartTitle) { + fails.push(r.heartItems === 0 + ? "point 4's reading list is NOT printed (no .p4-heart block)" + : `point 4's reading list prints ${r.heartItems} of ${HEART_ITEMS} readings${r.heartTitle ? '' : ', and no heading'}`) } // THE PRODUCT IS NAMED FOR THIS NUMBER. The page header says "12 Prayer Points" in every // locale, so 12 is a contract, not a preference — and the failure mode it guards is SILENT: @@ -870,8 +876,7 @@ console.log(` funnel (page 2) : ${r.funnel : 'absent' + (EXPECT_FUNNEL ? ' (REQUIRED — see FAIL above)' : ' (not required)')}`) console.log(` prayer points : ${r.points} / ${POINTS_EXPECT}`) console.log(` wheel segments : ${r.wheelSegs} (${r.wheelLabels}/${WHEEL_LABELS} labelled)`) -console.log(` Mark 5 in full : ${r.mark5Len} chars` - + `${scriptureIsEnglish ? ' ⚠ UNTRANSLATED (English fallback)' : ''}`) +console.log(` point 4's list : ${r.heartItems} readings (the full-passage block: ${r.mark5Len} chars, retired)`) console.log(` metacamp.org : ${r.metacamp}`) console.log(` map (page 1) : ${r.mapFaults.length ? `${r.mapFaults.join('; ')}${expectMap ? '' : ` [declared deliberate: ${/\bnomap\b/.test(build) ? 'data-packet-build="' + build + '"' : 'PACKET_EXPECT_MAP=0'}]`}` diff --git a/internal/check-poster-card-fits.mjs b/internal/check-poster-card-fits.mjs index 1a1aae019..3f9f08936 100644 --- a/internal/check-poster-card-fits.mjs +++ b/internal/check-poster-card-fits.mjs @@ -208,6 +208,7 @@ async function measure(width, lang) { let previewsPainted = 0, previewsBroken = 0, noPreview = 0, unnamedPreview = 0 let notTaped = 0, worstTape = 0, selectsInRow = 0 let headNotTaped = 0, worstHead = 0, mapCovered = 0, worstCover = 0 + let uncontained = 0, worstBelow = 0 for (const { c, b } of boxes) { const row = c.querySelector('.pfs') if (!row) continue @@ -216,6 +217,14 @@ async function measure(width, lang) { overflowing++ worst = Math.max(worst, Math.round(Math.max(rb.right - b.right, b.left - rb.left))) } + // ★ AND THE CARD CONTAINS ITS ROW (2026-09-30, from a phone at two posters per row). The row + // wrapped to three lines and hung about 45px below the card's white box, into the row gap: + // horizontally inside, so the check above stayed green. PosterWall now fits each card to its + // own row (--pfs-room); this is the assertion that keeps that true in every locale x width. + if (rb.bottom > b.bottom + 0.5) { + uncontained++ + worstBelow = Math.max(worstBelow, Math.round(rb.bottom - b.bottom)) + } // ⚠ THE VERTICAL AXIS TOO — this measured only left/right, which is exactly how it passed a // visible regression. Fixing the horizontal overflow with flex-wrap grew the row // 35px -> 78px, and because it sits at bottom:-14px the extra height pushed its TOP from @@ -364,7 +373,8 @@ async function measure(width, lang) { overflowing, worst, intrusions, coveringPreview, worstY, previewsPainted, previewsBroken, noPreview, unnamedPreview, clipped: clipped.length, controls: ctrls.length, tiny: tiny.length, perRow, cardW, - notTaped, worstTape, selectsInRow, headNotTaped, worstHead, mapCovered, worstCover } + notTaped, worstTape, selectsInRow, headNotTaped, worstHead, mapCovered, worstCover, + uncontained, worstBelow } })()`) await page.close() return { width, lang, hover, reach, ...out } @@ -382,6 +392,7 @@ console.log(` leaf : ${LEAF}`) for (const r of results) { console.log(` ${(r.lang || 'en').padEnd(2)} ${String(r.width).padStart(4)}px cards ${String(r.cards).padStart(2)}` + ` · overflow ${r.overflowing}${r.worst ? ` (${r.worst}px)` : ''}` + + ` · below-card ${r.uncontained}${r.worstBelow ? ` (${r.worstBelow}px)` : ''}` + ` · neighbour ${r.intrusions}` + ` · over-preview ${r.coveringPreview}${r.worstY ? ` (${r.worstY}px)` : ''}` + ` · off-frame ${r.notTaped}${r.worstTape ? ` (${r.worstTape > 0 ? '+' : ''}${r.worstTape}px)` : ''}` @@ -408,6 +419,7 @@ for (const r of results) { if (!r.rows) { fails.push(`${at}: ${r.cards} card(s) but none carry a .pfs print-options row`); continue } if (r.intrusions) fails.push(`${at}: ${r.intrusions} print row(s) overlap a NEIGHBOURING card`) if (r.overflowing) fails.push(`${at}: ${r.overflowing} print row(s) extend past their own card (worst ${r.worst}px)`) + if (r.uncontained) fails.push(`${at}: ${r.uncontained} print row(s) hang below their own card's box (worst ${r.worstBelow}px): the card must contain its row`) // ⚠ "Nothing overflows" is trivially satisfied by a row whose buttons collapsed to nothing. if (r.tiny) { fails.push(`${at}: ${r.tiny} print control(s) shrank below a usable size — fitting the row by ` diff --git a/internal/check-poster-prints.mjs b/internal/check-poster-prints.mjs index 5c44946f5..bf80b86b4 100644 --- a/internal/check-poster-prints.mjs +++ b/internal/check-poster-prints.mjs @@ -138,6 +138,8 @@ const r = await pg.evaluate(async ({ src, frame, packetRender, cardRender, qr, s // and check-packet-browser. Assert the passage BLOCK and its length, which holds in // any language. out.packetMark5Len = (pdoc.querySelector('.p4-passage') || { textContent: '' }).textContent.trim().length + // Point 4 prints THE HEAVENLY FATHER'S HEART (seven readings) in place of Mark 5:1-20 in full (2026-09-30). + out.packetHeartItems = pdoc.querySelectorAll('.p4-heart li').length // ── without a provider: must FAIL HONESTLY, not throw ──────────────────── const noProv = usePosterPrints({ label: (i) => i.label }) @@ -164,19 +166,18 @@ if (!/no records provider/i.test(r.noProvMsg)) { if (r.packetSheets !== 4) fails.push(`the packet printed ${r.packetSheets} sheets, expected 4`) if (r.packetCards !== 2) fails.push(`the packet printed ${r.packetCards} people cards, expected 2`) if (r.packetWheel !== 12) fails.push(`the prayer wheel has ${r.packetWheel} segments, expected 12`) -// The English passage is 2,141 chars; 1,200 is a floor no truncation or placeholder -// survives, with room for languages that render the same text more compactly. -if (r.packetMark5Len < 1200) { - fails.push(r.packetMark5Len === 0 - ? 'the packet is missing Mark 5:1-20 in full (no .p4-passage block)' - : `Mark 5:1-20 looks truncated: ${r.packetMark5Len} chars`) +// Point 4 prints THE HEAVENLY FATHER'S HEART, seven readings, in place of Mark 5:1-20 in full (2026-09-30). +if (r.packetHeartItems !== 7) { + fails.push(r.packetHeartItems === 0 + ? "the packet is missing point 4's reading list (no .p4-heart block)" + : `point 4's reading list prints ${r.packetHeartItems} of 7 readings`) } if (errs.length) fails.push(`${errs.length} page error(s): ${errs[0]}`) console.log(` list print : ${r.listRows} rows, checkbox=${r.listHasCheckbox}`) console.log(` without provider : phase=${r.noProvPhase} "${r.noProvMsg.slice(0, 60)}"`) console.log(` packet : ${r.packetSheets} sheets, ${r.packetCards} cards, ` - + `${r.packetWheel} wheel segments, Mark5=${r.packetMark5Len} chars`) + + `${r.packetWheel} wheel segments, point 4 list=${r.packetHeartItems}`) console.log(` page errors : ${errs.length}`) if (fails.length) { diff --git a/library/components/info/MapInfo.vue b/library/components/info/MapInfo.vue index f4e8c1fb1..f58736732 100644 --- a/library/components/info/MapInfo.vue +++ b/library/components/info/MapInfo.vue @@ -65,6 +65,14 @@ const props = defineProps({ takeaway: { type: String, default: '' }, /** A caveat under the examples (e.g. which countries are not supported yet). Key or string. */ noteKey: { type: String, default: '' }, + /** + * The map's STRATEGY, as a catalogue key PREFIX (e.g. 'mapInfo.tab.upg-100.strategy'): .bottom is + * the bottom line, .p1, .p2 … the points, read in order until the first one that is missing. So + * the number of points is the catalogue's business, never a count kept here. With a strategy the popup + * shows the intro (the lead), then the strategy; a map that also has examples gets two tabs, EXAMPLES + * first. The takeaway is not shown beside a strategy, which supersedes it. + */ + strategyKey: { type: String, default: '' }, note: { type: String, default: '' }, /** The artefacts, in display order. See the header for the four kinds. */ examples: { type: Array, default: () => [] }, @@ -106,6 +114,35 @@ function lang() { } const t = useOptionalT('', { fallback: (q) => tStatic(q, lang()) }) const text = (key, str) => (key ? t(key) : str) +// The strategy, read from the catalogue in the reader's language (tStatic falls back to English per key). +// lang() reads `opens`, so the lines are re-read on every open. A point is a non-empty string; the first +// missing one ends the list. +const MAX_POINTS = 8 +const strategyBottom = () => (props.strategyKey ? tStatic(props.strategyKey + '.bottom', lang()) || '' : '') +function strategyPoints() { + const out = [] + if (!props.strategyKey) return out + for (let i = 1; i <= MAX_POINTS; i++) { + const v = tStatic(props.strategyKey + '.p' + i, lang()) + if (!v) break + out.push(v) + } + return out +} +const hasStrategy = () => !!strategyBottom() +// Two tabs only where there is something on both sides: examples AND a strategy. +const hasTabs = () => hasStrategy() && (props.examples || []).length > 0 +const view = ref('examples') +// The ARIA tabs pattern: Left/Right move between the tabs and focus follows. With two tabs either key simply +// switches, so a right-to-left reader needs no mirrored mapping. +const tabIds = () => ({ ex: 'mi-tab-ex-' + props.storageKey, st: 'mi-tab-st-' + props.storageKey, panel: 'mi-panel-' + props.storageKey }) +function onTabKey(e) { + if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return + e.preventDefault() + const list = e.currentTarget // null once dispatch ends, so take it now, not inside nextTick + view.value = view.value === 'examples' ? 'strategy' : 'examples' + nextTick(() => { const btn = list && list.querySelector('[aria-selected="true"]'); if (btn) btn.focus() }) +} const num = (n) => { try { return new Intl.NumberFormat(lang()).format(n) } catch { return String(n) } } const isRtl = () => lang() === 'ar' @@ -162,6 +199,7 @@ const failed = reactive({}) async function show() { opens.value++ open.value = true + view.value = 'examples' // A page-load pointer is done for this load once the popup has been opened (in memory only). if (pageLoad()) pointerDoneThisLoad.add(props.storageKey) await nextTick() @@ -415,6 +453,16 @@ useShadowStyles(` .mi-takeaway{ margin:0 0 14px; color:#3b4256; } .mi-takeaway b{ color:#1b1f2a; } .mi-ex{ margin:18px 0 0; } + .mi-tabs{ display:flex; gap:4px; margin:14px 0 0; border-bottom:1px solid #e6e8ee; } + .mi-tab{ min-height:44px; padding:0 16px; border:none; background:none; color:#3b4256; font:inherit; font-weight:600; + cursor:pointer; border-bottom:3px solid transparent; margin-bottom:-1px; } + .mi-tab[aria-selected="true"]{ color:#1b1f2a; border-bottom-color:#3b463d; } + .mi-tab:focus-visible{ outline:3px solid #2563eb; outline-offset:-3px; } + .mi-strategy{ margin:16px 0 0; } + .mi-strategy h3{ margin:0 0 6px; } + .mi-bottom{ margin:0 0 8px; font-weight:600; color:#1b1f2a; } + .mi-points{ margin:0; padding-inline-start:20px; } + .mi-points li{ margin:0 0 6px; } .mi-ex h3{ margin:0 0 8px; font-size:15px; text-transform:uppercase; letter-spacing:.04em; color:#556; } .mi-img{ display:block; width:100%; height:auto; border-radius:8px; background:#f4f5f8; box-shadow:0 1px 4px rgba(0,0,0,.15); } @@ -517,8 +565,8 @@ useShadowStyles(` @click="show"> @@ -547,7 +595,26 @@ useShadowStyles(`

{{ text(leadKey, lead) }}

-

{{ t('mapInfo.takeaway') }} {{ text(takeawayKey, takeaway) }}

+

{{ t('mapInfo.takeaway') }} {{ text(takeawayKey, takeaway) }}

+ +
+ + +
+
+ +
+

{{ t('mapInfo.tabStrategy') }}

+

{{ strategyBottom() }}

+
    +
  • {{ p }}
  • +
+
+ + + +
+

{{ text(noteKey, note) }}

diff --git a/library/components/map-controls/MapInfoButton.vue b/library/components/map-controls/MapInfoButton.vue index 7fcf9d2e5..239642516 100644 --- a/library/components/map-controls/MapInfoButton.vue +++ b/library/components/map-controls/MapInfoButton.vue @@ -17,6 +17,9 @@ button (connected, a non-zero on-screen rect, not inert), never by an unmount event: a detached element cannot bubble anything to the pane. See MapInfo's control mode. + ★ A QUESTION MARK, NOT AN (i). The map's attribution control is a round (i) too, and two identical + glyphs on one map read as the same thing. The same glyph is MapInfo's floating fallback button. + Props: isDark — theme flag forwarded from parent --> @@ -102,8 +105,8 @@ onBeforeUnmount(() => { aria-haspopup="dialog" aria-expanded="false" @click="openInfo"> diff --git a/library/components/maps/RegionCountryMap.vue b/library/components/maps/RegionCountryMap.vue index ba0c8459e..7fbdeacf6 100644 --- a/library/components/maps/RegionCountryMap.vue +++ b/library/components/maps/RegionCountryMap.vue @@ -258,9 +258,16 @@ function pinExpressions() { // visible loss of contrast and buys nothing when nothing is being de-emphasised yet. // The HOVER state is deliberately untouched — dimming the rest to 0.25 is the design saying // "these pins are one group", which is a taste call rather than a legibility defect. + // ★ SMALLER BELOW ZOOM 1 (2026-09-30: on a phone the globe sits near zoom -0.5 and the dots, clamped at their + // zoom-2 size, read as far too large). Measured: the desktop chooser sits near zoom 1.1, so every stop from zoom + // 1 up keeps the old size and the desktop keeps its dots; below zoom 1 they shrink to about half. radius: lit - ? ['case', isLit, ['interpolate', ['linear'], ['zoom'], 2, 4.5, 6, 8], ['interpolate', ['linear'], ['zoom'], 2, 3, 6, 5.5]] - : ['interpolate', ['linear'], ['zoom'], 2, 4, 6, 7.5], + // ZOOM AT THE TOP, the case inside each stop: MapLibre allows ONE zoom interpolate per expression, at the + // top, so ['case', isLit, interpolate, interpolate] does not parse and a lit map never got these sizes. + ? ['interpolate', ['linear'], ['zoom'], -1, ['case', isLit, 2.8, 1.6], 0.5, ['case', isLit, 3.2, 2], 1, ['case', isLit, 4.5, 3], 2, ['case', isLit, 4.5, 3], 6, ['case', isLit, 8, 5.5]] + : ['interpolate', ['linear'], ['zoom'], -1, 2, 0.5, 2.5, 1, 4, 2, 4, 6, 7.5], + // THE TAP TARGET: the invisible rcm-pins-hit twin (addLayers), at least 7 px at every zoom. + hitRadius: HIT_RADIUS, } } // DX-life's two modes, unchanged (CountryMap.vue:218-223). @@ -274,13 +281,19 @@ function pinExpressions() { opacity: ['case', ['get', 'inCountry'], 1, 0.75], strokeOpacity: 1, // theirs, unchanged (CountryMap.vue) — only the chooser's dim is ours radius: ['interpolate', ['linear'], ['zoom'], 2, 3, 6, 6.5], + hitRadius: HIT_RADIUS, } } +// ★ PIN TAPS WERE HARD TO LAND ON A PHONE ("not very responsive"): the tap target was the drawn dot, 3-4 px of +// radius at the zooms a phone shows. The invisible twin takes at least 7 px at every zoom (the size UPG-100's pins +// settled on), growing with the dots at street zoom. Where pins crowd, the pin drawn on top wins the tap. +const HIT_RADIUS = ['interpolate', ['linear'], ['zoom'], -1, 7, 2, 7, 6, 9] + function addLayers() { const m = map.value if (!m) return - const { color: pinColor, sortKey: pinSortKey, opacity: pinOpacity, radius: pinRadius, strokeOpacity: pinStrokeOpacity } = pinExpressions() + const { color: pinColor, sortKey: pinSortKey, opacity: pinOpacity, radius: pinRadius, strokeOpacity: pinStrokeOpacity, hitRadius: pinHitRadius } = pinExpressions() if (!m.getSource(SRC_COUNTRIES)) { // ── COUNTRY POLYGONS, PER PROVIDER ──────────────────────────────────────────────────────── @@ -358,9 +371,21 @@ function addLayers() { 'circle-stroke-opacity': pinStrokeOpacity, }, }) - m.on('click', 'rcm-pins', onPinClick) - m.on('mouseenter', 'rcm-pins', () => { m.getCanvas().style.cursor = 'pointer' }) - m.on('mouseleave', 'rcm-pins', () => { m.getCanvas().style.cursor = '' }) + // ★ AN INVISIBLE HIT TWIN (the pattern UPG-100's pins use): the engines hit-test a circle at its radius + + // stroke, so shrinking the drawn dot would shrink the tap target with it. The twin keeps the old ramps, the + // same sort key (so a tap picks the same pin as before), and both opacities 0, which skips the draw but + // keeps the features queryable. The click and the cursor bind to it. + m.addLayer({ + id: 'rcm-pins-hit', type: 'circle', source: SRC_PINS, + layout: { 'circle-sort-key': pinSortKey }, + paint: { 'circle-radius': pinHitRadius, 'circle-stroke-width': 1, 'circle-opacity': 0, 'circle-stroke-opacity': 0 }, + }) + m.on('click', 'rcm-pins-hit', onPinClick) + m.on('click', 'rcm-focus-fill', onCountryClick) + m.on('mouseenter', 'rcm-focus-fill', () => { if (props.selectOnPinClick) m.getCanvas().style.cursor = 'pointer' }) + m.on('mouseleave', 'rcm-focus-fill', () => { if (props.selectOnPinClick) m.getCanvas().style.cursor = '' }) + m.on('mouseenter', 'rcm-pins-hit', () => { m.getCanvas().style.cursor = 'pointer' }) + m.on('mouseleave', 'rcm-pins-hit', () => { m.getCanvas().style.cursor = '' }) } } @@ -369,13 +394,9 @@ let popup = null function onPinClick(e) { const f = e.features && e.features[0] if (!f) return - // CHOOSER: a pin click is a CHOICE, not a popup — the whole point of the state is to pick a - // subject. On a cloned page it stays their popup, because that is what their page does. - if (props.selectOnPinClick) { - const p = f.properties || {} - emit('pick-country', { code: p.code || '', region: p.region || '', name: p.name || '' }) - return - } + // ★ A PIN TAP OPENS ITS POPUP, everywhere (2026-09-30: on the all-regions map a pin tap jumped to the country's + // dashboard, when the reader wanted the people group). In the chooser, a COUNTRY tap picks the country instead + // (onCountryClick). // ★ THE ENGINE THAT BUILT THIS MAP, not the Mapbox global. This read `window.mapboxgl` and // returned SILENTLY when it was absent. Under provider=maplibre it worked only because every // leaf statically loads mapbox-gl — so a MapLibre map was opening a MAPBOX popup. studio @@ -393,6 +414,16 @@ function onPinClick(e) { requestAnimationFrame(() => panPopupIntoView(f.geometry.coordinates)) } +// The CHOOSER picks a country by its shape. A tap that lands on a pin is the pin's (its popup), so the country +// only answers a tap with no pin under it. The prop keeps its old name: it means "this map is the chooser". +function onCountryClick(e) { + if (!props.selectOnPinClick || !map.value) return + if (map.value.getLayer('rcm-pins-hit') && map.value.queryRenderedFeatures(e.point, { layers: ['rcm-pins-hit'] }).length) return + const f = e.features && e.features[0] + const code = f && f.properties && f.properties.iso_3166_1_alpha_3 + if (code) emit('pick-country', { code: String(code).toUpperCase(), region: '', name: (f.properties.name || '') }) +} + // Their popup offset, as one constant: the pan arithmetic has to use the SAME number the popup was // built with, and two literals drift. const POPUP_OFFSET = 12 @@ -768,12 +799,16 @@ function applySubject() { if (src) src.setData(geojson.value) // Re-apply the MODE, not just the data — see pinExpressions(). if (m.getLayer('rcm-pins')) { - const { color, sortKey, opacity, radius, strokeOpacity } = pinExpressions() + const { color, sortKey, opacity, radius, strokeOpacity, hitRadius } = pinExpressions() m.setPaintProperty('rcm-pins', 'circle-color', color) m.setPaintProperty('rcm-pins', 'circle-opacity', opacity) m.setPaintProperty('rcm-pins', 'circle-stroke-opacity', strokeOpacity) m.setPaintProperty('rcm-pins', 'circle-radius', radius) m.setLayoutProperty('rcm-pins', 'circle-sort-key', sortKey) + if (m.getLayer('rcm-pins-hit')) { + m.setPaintProperty('rcm-pins-hit', 'circle-radius', hitRadius) + m.setLayoutProperty('rcm-pins-hit', 'circle-sort-key', sortKey) + } } for (const id of ['rcm-focus-fill', 'rcm-focus-outline']) { if (m.getLayer(id)) m.setFilter(id, countryFilter()) diff --git a/library/components/poster/PosterDialog.vue b/library/components/poster/PosterDialog.vue index 730ad0d49..a34ae2bac 100644 --- a/library/components/poster/PosterDialog.vue +++ b/library/components/poster/PosterDialog.vue @@ -32,9 +32,10 @@ +

{{ T('poster.phoneLimit', "On a phone, the largest sizes are off: a phone can't draw a poster that big. Use a computer for them.") }}