Keep the reason, drop the speaker, in the UPG-100 and Country Posters plugins

The 40 baselined references in my-upg-100-list (32) and country-posters (8),
plus 3 unbaselined ones in data/*.js, each wrapped an engineering reason in
an attribution. The reason stays; the speaker, the quotation marks and the
pronouns go. Dates and measurements are kept where they were ("decided
2026-09-24", "Reported 2026-09-24: the UPG-100 list legend on the map
overflowed beyond the height of the map"). Where a quote carried a
requirement, the requirement is now stated directly.

Comment-only, one line for one line (73 against 73), including the
meta.json note, which does not reach the manifest. Both bundles build
byte-identical to the base. Route test: 0 files under library/ or
internal/.
This commit is contained in:
Doxa Maps 2026-09-29 08:33:42 +00:00
parent 4ea988a4c9
commit 538e2fb782
8 changed files with 73 additions and 73 deletions

View file

@ -369,7 +369,7 @@ const profileConfig = inject('profileConfig')
// ── SEARCH BAR IS MAPBOX-ONLY, AND ITS ABSENCE MUST NOT LEAVE A HOLE ─────────
// Same contract as doxa-research-map (the reference implementation), deliberately copied rather
// than re-invented. Driver 2026-09-24: the geocoder is DEFERRED to Mapbox, because
// than re-invented. Decided 2026-09-24: the geocoder is DEFERRED to Mapbox, because
// GeocoderComponent's maplibre path still calls makeMapboxForwardGeocode(accessToken) — the search
// REQUEST goes to Mapbox whichever engine draws the tiles, so a keyless map with a search bar is
// not keyless and would bill the very key the maplibre option exists to avoid.
@ -382,7 +382,7 @@ const hasGeocoder = computed(() => {
})
// SemanticTreeLegend's --stl-top defaults to 60px, and that 60px exists ONLY to reserve room for a
// search bar above it. With no bar the legend must RISE into the vacated space instead of leaving a
// dead band — the Driver's explicit requirement. 14px matches the reference.
// dead band — a requirement, not a preference. 14px matches the reference.
// How far the legend may rise depends on WHAT ELSE is in the strip the search bar vacated.
// LegendTools sits at top:14px on the tabs its predicate allows, so raising the legend to 14 there
// prints the toolbar across the legend's header — MEASURED on country-posters. Ask the toolbar's own
@ -1236,8 +1236,8 @@ async function openPacket(variant = 'full') {
if (_packetBusy) return
_packetBusy = true
try {
// ★ THE PACKET PRINTS, AND IT PRINTS THROUGH THE SHARED RENDERER (Driver-lens audit card 2, and
// the Driver's own rule: an export button opens the print dialog, never hands over a file). This
// ★ THE PACKET PRINTS, AND IT PRINTS THROUGH THE SHARED RENDERER (product-lens audit card 2, and
// the product rule: an export button opens the print dialog, never hands over a file). This
// built a jsPDF document and downloaded packet.pdf while its poster sibling, two functions up,
// already printed. The fix is NOT to wrap that PDF in a dialog — a PDF blob cannot go in the
// <img> the in-place print frame uses, and every workaround (embed, object, the browser's own

View file

@ -5,7 +5,7 @@
"country-posters/country-poster-wall",
"country-posters/countries-map"
],
"note": "The Driver's 2-tab Country Posters element, structured like my-upg-100-list/upg-100-mfe: a SELECT VIEW switcher (the SHARED library/components/ViewSelect.vue — this is its THIRD consumer, which is why it was extracted rather than copied a third time) over Country Posters at [0,0] and Map at [1,0]. LIST FIRST, MAP SECOND per the Driver's principle that maps are a secondary view. The wall already dispatches 'doxa-country-posters:show-on-map' (country-poster-wall.vue:78); until now only the app shell listened, so the action did nothing in a dropped embed — this shell owns it and swaps to the map tab. pinHint:false: the cloned panes carry no coach-mark, and the composite must declare it because a pane's config comes from here, not from the child's own meta. One ShareButton in the header, sharing the COMPOSITE leaf, per the recorded share policy.",
"note": "The 2-tab Country Posters element, structured like my-upg-100-list/upg-100-mfe: a SELECT VIEW switcher (the SHARED library/components/ViewSelect.vue — this is its THIRD consumer, which is why it was extracted rather than copied a third time) over Country Posters at [0,0] and Map at [1,0]. LIST FIRST, MAP SECOND, on the principle that maps are a secondary view. The wall already dispatches 'doxa-country-posters:show-on-map' (country-poster-wall.vue:78); until now only the app shell listened, so the action did nothing in a dropped embed — this shell owns it and swaps to the map tab. pinHint:false: the cloned panes carry no coach-mark, and the composite must declare it because a pane's config comes from here, not from the child's own meta. One ShareButton in the header, sharing the COMPOSITE leaf, per the recorded share policy.",
"config": {
"pinHint": false
}

View file

@ -7,8 +7,8 @@
* poster wall behind a single view switcher, structured like the UPG-100 list. The switcher is
* the library's shared ViewSelect, not a third hand-rolled copy.
*
* Driver: "Country Posters is supposed to have two tabs, structured like the
* UPG-100 list."
* Country Posters has two tabs, structured like the
* UPG-100 list.
*
* ★ THE CONTROL IS SHARED, NOT COPIED. That is the whole point of the card: this
* is the THIRD place the SELECT VIEW switcher is needed, and it uses the library's
@ -25,9 +25,9 @@
* • one ShareButton in the header, per the recorded share policy
* (check:share-policy) — the MAP pane must not grow a second one
*
* LIST FIRST, MAP SECOND. The Driver's principle, already applied in upg-100-mfe:
* "maps don't have to be primary. You can use lists and grids for the primary view
* and maps as a secondary view… then they transition into the map." The poster wall
* LIST FIRST, MAP SECOND. The principle already applied in upg-100-mfe: a map need
* not be the primary view — a list or a grid can be, with the map secondary and
* reached as a transition from it. The poster wall
* is the concrete artefact a newcomer can act on; the map is where it is placed.
*/
import { ref, computed, inject, onMounted, onBeforeUnmount } from 'vue'
@ -239,7 +239,7 @@ useShadowStyles(PANE_GRID_STYLES + `
no such rule exists and every pane collapses to 0 — the "elements are not embedding"
report. Declaring :host makes the element fill its slot in every deploy method. */
:host{ display:block; width:100%; height:100%; min-height:420px; }
/* ── palette: DX-life material light. NO AUTO-DARK (Driver 2026-09-20): a system set to
/* ── palette: DX-life material light. NO AUTO-DARK (decided 2026-09-20): a system set to
dark mode must not flip this element. ── */
.cpmfe{
--ui-bg:#ffffff; --ui-bg-elevated:#f3f3f1; --ui-bg-accented:#f3f3f1;

View file

@ -109,7 +109,7 @@ export const REGION_KEY = {
export function buildLegendNodes(clusters, opts = {}) {
const deafCount = opts.deafCount ?? 123;
const records = Array.isArray(opts.records) ? opts.records : null;
// ★ OPTIONAL GENERATED TITLE (Driver, 2026-09-27). `opts.title(zone)` returns a localized title
// ★ OPTIONAL GENERATED TITLE (2026-09-27). `opts.title(zone)` returns a localized title
// composed from already-translated parts, or NULL for a zone whose title cannot be generated — see
// data/zone-title.js. Passing nothing keeps every hand-written English label exactly as it was, which
// is why this is an option rather than a rewrite: the legend, the poster and the packet adopt it

View file

@ -30,7 +30,7 @@
* Then `npm run check:zoning-declared -- --list <id>` grades the zoning, and the legend, poster wall and
* packet all follow, because all three already enumerate through leafZones(buildLegendNodes(zones)).
*
* ZONING RULE, from the Driver (2026-09-27), recorded here because it is the judgement the format cannot
* ZONING RULE (2026-09-27), recorded here because it is the judgement the format cannot
* capture: cluster by GEOGRAPHY FIRST so the poster maps make sense, balanced with LANGUAGE — languages
* usually cluster geographically, so the two mostly agree, and where they disagree geography wins for a
* map's sake. UPG-100's own 28 already mix the two: the country_codes zones are geographic, and the 12
@ -38,8 +38,8 @@
*
* ⚠ THE DEAF ZONE IS THE KNOWN EXCEPTION AND MUST NOT BE FORCED. `deaf` is affinity bloc A017: 123 groups
* spanning every region, global by construction, and the one poster whose zone cannot be a map extent. It
* breaks the geography-first rule the rest of the pattern rests on, and the Driver has said he has not
* solved it. A list whose clusters include an affinity or language bloc inherits the same open question —
* breaks the geography-first rule the rest of the pattern rests on, and it is recorded as not yet
* solved. A list whose clusters include an affinity or language bloc inherits the same open question —
* declare it in bloc_zones so the gate treats it as a second partition, and do NOT invent a geography for
* it to make a validator happy.
*

View file

@ -24,10 +24,10 @@
* cardStocks — the reader picks the prayer-card size at print time.
* posterAction="print" — open the print dialog instead of saving a file.
* hidePrayerListWhenSoon — no "coming soon" placeholder on a wall whose list DOES print.
* The last two are a Driver ruling, not a preference (2026-09-23): "three print buttons, each
* opening an in-browser print dialog, no silent file download as the default action. A raw
* 'Download' button and a 'Prayer List: coming soon' placeholder were exactly the two buttons he
* objected to."
* The last two are a product ruling, not a preference (2026-09-23): three print buttons, each
* opening an in-browser print dialog, and no silent file download as the default action. A raw
* 'Download' button and a 'Prayer List: coming soon' placeholder were exactly the two buttons
* ruled out.
*
* CROSS-PANE COMMS — unchanged and still the decoupling that matters: this wall NEVER imports the
* map. The shared component dispatches the window CustomEvent named below, scoped by instanceId,
@ -210,7 +210,7 @@ const zoneSheet = makeZoneSheet({ allRecords: () => recordsFor.all(), assetsBase
// catalogued — I told regions-plugin exactly that, and it was FALSE: this file's three keys are
// present in all ELEVEN locales, 0 gaps (enumerated 2026-09-27; the only three, all string literals).
// ⚠ IT GUARDS A BARE HOST, NOT A MISSING KEY. Those are different failures and I had them confused.
// Product call taken by dx-shepherd (UPGHK, 2026-09-27) under the Driver's standing goal that the
// Product call taken by dx-shepherd (UPGHK, 2026-09-27) under the standing goal that the
// production maps be localized.
const t = useOptionalT('')

View file

@ -167,7 +167,7 @@ function testHandleKey() {
// the ENGINES table in a profile is how two copies of one fact drift apart.
const upgProvider = resolveMapProvider(profileConfig)
const isMaplibre = upgProvider === 'maplibre'
// The geocoder is deferred to Mapbox by decision (Driver 2026-09-24): GeocoderComponent's maplibre
// The geocoder is deferred to Mapbox by decision (2026-09-24): GeocoderComponent's maplibre
// path still calls makeMapboxForwardGeocode(accessToken), so the SEARCH REQUEST goes to Mapbox
// whichever engine draws the tiles — a keyless map with a search bar is not keyless.
const hasGeocoder = computed(() => {
@ -349,7 +349,7 @@ const geocoderRef = ref(null) // <GeocoderComponent> exposed ref
// catalog entry, so the labels below are what render. If these tabs are ever translated, add
// `upg100Region`/`upg100Zone`/`upg100Group` to tabs.json rather than reusing a generic key.
// ★ "Zone" RENAMED, ORDER DELIBERATELY UNCHANGED (2026-09-23).
// Driver: "the user does not know what zones means" — the thing the map paints IS
// A reader does not know what "zones" means — the thing the map paints IS
// a UPG-100 list (27 of them); "zone" was our internal word leaking into the UI.
// So the LABEL now names what it shows. Region stays as what it actually is: a way
// to SORT those lists by world area when you want that.
@ -383,7 +383,7 @@ const LEGEND_TABS = computed(() => [
// MEASURED 2026-09-25 by expanding every caret on the Region tab — 37 rows, 9 at indent 0 and
// 28 at indent 1, deepest level 1. There is no level 2, so tab index 2 resolved to an empty set
// and the panel rendered its "No data at this level" empty state with the tab ACTIVE.
// The Driver said "it doesn't need to have a People Group tab inside the legend"; the reason is
// The legend does not need a People Group tab; the reason is
// that clicking it showed nothing at all.
// ⚠ IF PEOPLE-GROUP ROWS ARE EVER WANTED BACK, a third tab is not the mechanism — the tree would
// need a third level (list -> group). Re-adding the tab without deepening the data reproduces
@ -442,8 +442,8 @@ function legendOpts(records) {
// the caller owns localizing it. MEASURED 2026-09-25 on the live Chinese render: the legend
// header read "My UPG 100 List — Legend" in full English inside an otherwise localized panel,
// beside translated tabs (区域 · UPG-100 名单) and a translated column header (人口).
// The Driver's gift policy is explicit — "BEAUTIFUL: no clipped text, no placeholders, no English
// leaking" — and an English title on the mobile showcase is exactly that.
// The gift policy is explicit — beautiful: no clipped text, no placeholders, no English
// leaking — and an English title on the mobile showcase is exactly that.
// ★ THE HOUSE STYLE FOR A PROPER NOUN INSIDE A TRANSLATED PHRASE ALREADY EXISTS in this catalog:
// "Doxa Regions" renders as "Doxa 区域" / "مناطق Doxa" / "Регионы Doxa" — the identifier stays
// Latin, the frame translates. "UPG 100" is treated the same way rather than transliterated, so
@ -500,8 +500,8 @@ useShadowStyles(
// matches research-map's .rm-legend-*-slot (767px) so the whole map stack
// flips at the same width instead of drifting per profile.
// ★ The search bar above the legend was REMOVED from this profile, so the legend rises into the
// vacated space (Driver 2026-09-23: "the search bar is removed... the legend needs to be moved
// up so that there's not a gap where the search bar used to be"). --stl-top is the shared
// vacated space (2026-09-23: with the search bar removed, the legend moves
// up so that no gap is left where the search bar used to be). --stl-top is the shared
// SemanticTreeLegend's offset-from-map-top; 8px matches its own left inset. Other profiles keep
// the 60px default because they still have their search bar.
'.upg-legend-desktop-slot{--stl-top:8px}' +
@ -626,12 +626,12 @@ useShadowStyles(
'max-width:380px!important;z-index:1200!important}' +
'.mapboxgl-ctrl-top-left .mapboxgl-ctrl{margin:0!important;float:none!important}' +
// Push the legend panel BELOW the geocoder pill (legend is top-left here too) —
// ★ BUT ONLY IF THE PILL ACTUALLY RENDERED (Driver 2026-09-23, reported repeatedly: "the search
// bar is removed, so the legend needs to move up — there's a gap where the search bar used to
// be"). MEASURED in both hosts with a real token: the standalone leaf mounts the geocoder
// ★ BUT ONLY IF THE PILL ACTUALLY RENDERED (2026-09-23, reported repeatedly: with the search
// bar removed, the legend must move up — no gap where the search bar used to
// be). MEASURED in both hosts with a real token: the standalone leaf mounts the geocoder
// (top 12, h 35) and the legend belongs at 58; the cap-5 shell mounts NO geocoder at all, and
// an unconditional 58px stranded the legend below a pill that was not there — the dead gap the
// Driver kept reporting. One host cannot be fixed by breaking the other, so the offset is
// reports kept naming. One host cannot be fixed by breaking the other, so the offset is
// conditional on the pill EXISTING: default raised to 8px (matching .stl-panel's own left
// inset), pushed down only when a rendered geocoder is actually in the tree.
// ★ SET --stl-top, DO NOT SET top DIRECTLY. The library caps the panel at
@ -641,8 +641,8 @@ useShadowStyles(
// MEASURED 2026-09-24, upg-100-list hosted in a 420px-tall frame at 390px wide: top resolved to
// 58px while max-height resolved to calc(100% - 20px), so the panel ran to 458px in a 420px map
// and HUNG 38px PAST THE BOTTOM OF THE MAP — exactly 58 - 20. Same 38px at a 560px frame, because
// the error is the offset mismatch, not the map's size. Driver 2026-09-24: "the upg 100 list
// legend on the map is overflowing beyond the height of the map".
// the error is the offset mismatch, not the map's size. Reported 2026-09-24: the UPG-100 list
// legend on the map overflowed beyond the height of the map.
// ⚠ WHY THIS IS INVISIBLE ON EVERY PUBLISHED LEAF: a standalone leaf gives the map the FULL
// viewport, and a panel that overflows a full-height map is still inside the screen. It only
// shows when a host makes the map SHORTER than the viewport, which is how the element is really
@ -670,7 +670,7 @@ useShadowStyles(
// measurement — correctly. rega will read the rows on Simple Research and Prayer Progress, which
// have NO override, after landing 5. If they read RTL there, DELETE the two rules below too.
// ── the original finding, kept because it explains the shape ────────────────
// Driver-reported via repo, 2026-09-26, from the README's Arabic picture: under ?lang=ar the legend
// Reported via repo, 2026-09-26, from the README's Arabic picture: under ?lang=ar the legend
// panel stayed on the LEFT and the map controls sat on top of its top-left corner.
//
// ★ THE CAUSE IS ONE COMPONENT BEING RTL-AWARE AND ITS NEIGHBOUR NOT. MapToolbar.vue:42 already uses
@ -1315,15 +1315,15 @@ provide('onSemanticTreeSelect', onLegendSelect)
// prop because <LegendMobile> renders several levels down inside the shell, which is also
// why onSemanticTreeSelect above is provided. Without this the mobile sheet rendered its
// SemanticTreeLegend with exportEnabled defaulting to FALSE, so a phone reader could browse
// the lists and had no way to print a packet or a poster — the half of this profile the
// Driver called "the only way that map would be usable".
// the lists and had no way to print a packet or a poster — the half of this profile without
// which the map is not usable.
provide('onLegendExport', onLegendExport)
provide('legendExportCards', true) // the card deck prints for the selected list
// ── LEGEND EXPORT (top-right of the legend titlebar) ────────────────────────
// The shared legend emits 'export' with 'poster' | 'packet' for the currently
// SELECTED node — the Driver's ask: "I select one and it gives me print options at
// the top right of the legend... that's the only way that map would be usable."
// SELECTED node — the requirement: select one and its print options appear at
// the top right of the legend; without that the map is not usable.
//
// Poster serves the SAME pre-rendered artefact the poster wall downloads — one
// file, so the legend, the wall and the preview can never show three different
@ -1333,14 +1333,14 @@ provide('legendExportCards', true) // the card deck prints for the selected li
// One packet is ~3.5 MB (photos dominate); 28 zones would add ~100 MB to a repo
// that ALREADY had a publish blocked at a 74 MB asset tree. Generating on click
// costs zero repository bytes, and the photos come from URLs the records already
// carry. This is the Driver's instruction: "instead of fetching the poster from
// the public folder I want the client side code to generate the poster real time
// when you click on the button so it does not take a lot of mb storage."
// carry. The requirement: instead of fetching the poster from
// the public folder, the client generates the poster in real time
// at the click, so it does not cost megabytes of storage.
//
// The pages themselves come from library/packet/packet-render.js — the SAME module
// the build-time generator uses, so the printed packet and the generated one cannot
// drift. The card asked for exactly that: reuse, do not grow a fourth print path.
// ★ PRINT STRAIGHT FROM THE MAP — the Driver's "that's the only way that map would be usable".
// ★ PRINT STRAIGHT FROM THE MAP — without it the map is not usable.
// Select a UPG-100 list in the legend and its print options appear at the legend's top right.
//
// ⚠ THE POSTER PATH USED TO HAND-ROLL AN <a download> OF THE .webp, which was wrong twice:
@ -1526,7 +1526,7 @@ async function localizedZone(zone) {
// Every list across all three cluster buckets, flattened.
//
// ★ THE ONE SEAM THE `list` PROP WILL FEED. Driver 2026-09-26 via the shepherd: UPG-100 is the mass
// ★ THE ONE SEAM THE `list` PROP WILL FEED. Decided 2026-09-26: UPG-100 is the mass
// prayer mobiliser — a GENERATOR that takes a list in and puts posters, packets and cards out, so a
// partner can generate their own. Every list-consuming path in this profile goes through THIS
// function (allLists -> selectedLists, printPrayerPacket, the legend, the funnel), so when the
@ -1627,7 +1627,7 @@ async function packetFunnel() {
// A confident figure I cannot defend is exactly the defect class I have been removing from other
// surfaces all day; this one I put here myself. funnelPage already falls back to the
// provenance-only footnote when unassigned is absent, so the page simply stops claiming it.
// Carded for the Driver: 27 geographic / 29 counting deaf, and whether "Central Asia + Pakistan"
// Carded as a product question: 27 geographic / 29 counting deaf, and whether "Central Asia + Pakistan"
// (55 groups, contiguous, poster-walled as a real region) is a prayer list rather than a residual.
if (listId === LIST_DEFAULT) {
// printableZones, not allLists: the count prints on page 1 ("one of N") and at the triangle's
@ -1656,9 +1656,9 @@ async function packetFunnel() {
}
// ── BATCH SELECTION MAKES A PACKET OR A POSTER ──────────────────────────────
// Driver: the batch tool is "super buggy", and recolouring regions is not what is wanted —
// "the batch selection tool should let you create your own poster or prayer packet based on
// your own selection." So the selection is an INPUT to the two artefacts this app already
// The batch tool was reported very buggy, and recolouring regions is not what is wanted —
// the batch selection tool should let a reader create their own poster or prayer packet from
// their own selection. So the selection is an INPUT to the two artefacts this app already
// builds, and neither of these grows a third pipeline: both end in printPrayerPacket /
// packetPrints.printPoster, the same calls the legend and the poster wall make.
const batchBusy = ref(false);
@ -1747,7 +1747,7 @@ async function selectionAsList() {
throw keyedError('upgMap.nothingSelected', 'nothing is selected');
}
if (!API_BASE) throw keyedError('wall.hud.noApi', 'no people-groups service is configured');
// ★ primary_language + imb_gsec FEED THE GENERATED PROFILE DESCRIPTION (Driver brief A1.5 col 2,
// ★ primary_language + imb_gsec FEED THE GENERATED PROFILE DESCRIPTION (brief A1.5 col 2,
// 2026-09-28). describeGroup builds a localized sentence — who, where, what language, how reached —
// and GSEC is the only reachedness field that is a NUMBER: EvangelicalLevel is English prose from the
// API ('2% or Greater but Less than 5%'), so interpolating it would leave every non-English packet
@ -2322,7 +2322,7 @@ onMounted(async () => {
// the shadow root v3.3.0, .mapboxgl-canvas-container position:STATIC where Mapbox declares
// it absolute, .mapboxgl-canary display:block, and 0 markers. A canvas container with no
// positioning context leaves every absolutely-positioned child — the pins — with nothing to
// anchor to. Driver: "the polygons for the map and map colors and pins are not showing up".
// anchor to. Reported: the map's polygons, colours and pins were not showing up.
// The comment at the top of this file already says the engine loader is imported rather than
// re-declared "because a second copy of one fact drifts"; the stylesheet URL was that second
// copy, and it drifted.
@ -2642,9 +2642,9 @@ onMounted(async () => {
// nothing here. Sized for the count, not copied from the sibling: 6px at z=1 (+50%),
// tapering back to the original 9px by z=8, where geography supplies the context and the
// dots no longer need to carry it alone.
// ★ REVERTED TOWARD THE SIBLINGS (Driver 2026-09-23: "the dots are way too big, I never
// asked for them to be that big, they're larger than the dots on most of the other maps").
// He is right and this was my overcorrection: an earlier pass raised z=1 from 4 to 6 to fix
// ★ REVERTED TOWARD THE SIBLINGS (2026-09-23: the dots were far too big, larger than
// the dots on most of the other maps, and nobody had asked for that size).
// That report is right and this was my overcorrection: an earlier pass raised z=1 from 4 to 6 to fix
// "hard to see", reasoning that 100 marks alone on a planet need more presence than
// all-people-groups' 12,379. The reasoning was sound but the amount was not — at 6 this map
// became the LARGEST of the set (all-people-groups is 2.2 at z=0, 3.4 at z=4), so it read as
@ -2965,9 +2965,9 @@ onBeforeUnmount(() => {
<LocationButton :map="mapRef" :is-dark="isDark" />
<FullscreenButton :map-container="mapEl" :is-dark="isDark" />
<ThemeToggleButton :is-dark="isDark" @toggle="handleToggleTheme" />
<!-- ★ NO MAP-LEVEL SHARE BUTTON HERE (Driver 2026-09-23, reported twice): "there's a share
button on the map, but there's ALSO a share button on the header of the website. That's
two separate share buttons. We don't need the one on the map."
<!-- ★ NO MAP-LEVEL SHARE BUTTON HERE (2026-09-23, reported twice): there was a share
button on the map AND a share button in the header of the website — two separate
share buttons, and the one on the map is not needed.
The composite upg-100-mfe.vue:217 already mounts a ShareButton in the header, and that is
the one to keep — it shares the whole cap-4 element, whereas this one shared only the map
half, so the duplicate was also the less useful of the two.
@ -3111,9 +3111,9 @@ onBeforeUnmount(() => {
</span>
</div>
<div v-if="selCount" class="upg-hud-names">{{ selectedTerritories.map(t => t.name).join(', ') }}</div>
<!-- ★ THE SELECTION MAKES SOMETHING. Driver: the batch tool is "super buggy", and letting
you change the colour of regions is NOT what is wanted — "the batch selection tool
should let you create your own poster or prayer packet based on your own selection."
<!-- ★ THE SELECTION MAKES SOMETHING. The batch tool was reported very buggy, and letting
a reader change the colour of regions is NOT what is wanted — the batch selection tool
should let them create their own poster or prayer packet from their own selection.
So the colour swatches and the hex input are GONE, and the actions that remain BUILD
the two artefacts this app already knows how to make, from exactly what was picked.
These reuse printPrayerPacket / printPoster rather than growing a third pipeline. -->

View file

@ -62,7 +62,7 @@ import ShareButton from '@map/components/map-controls/ShareButton.vue'
// reader's locale and still ends at English.
// The four keys this file uses (tabs.map / prayerResources / selectView / views) are present in all
// ELEVEN locales, 0 gaps, enumerated 2026-09-27 — nothing here was relying on the fallback.
// Product call taken by dx-shepherd (UPGHK, 2026-09-27) under the Driver's standing goal that the
// Product call taken by dx-shepherd (UPGHK, 2026-09-27) under the standing goal that the
// production maps be localized; it changes what seven vue-i18n-less bundles render, which is not a
// decision a migration gets to make for itself.
const t = useOptionalT('')
@ -103,12 +103,12 @@ const spec = computed(() => ({
instanceId: instanceId.value,
...(cfg.value?.dataSource ? { dataSource: cfg.value.dataSource } : {}),
},
// RESOURCES FIRST, MAP SECOND (Driver 2026-09-23). "By default the first tab
// shouldn't be the map. The first tab should be the prayer resources" — that pane
// RESOURCES FIRST, MAP SECOND (2026-09-23). By default the first tab is not the
// map; it is the prayer resources — that pane
// already explains what a UPG-100 list IS at the top and shows the posters, so it
// needs no explanation; a choropleth of zones does. Generalised by him as a design
// principle: "maps don't have to be primary. You can use lists and grids for the
// primary view and maps as a secondary view… then they transition into the map."
// needs no explanation; a choropleth of zones does. Generalised as a design
// principle: a map need not be primary. A list or a grid can be the
// primary view, with the map secondary and reached as a transition from it.
//
// ORDER LIVES IN `at[0]`, NOT IN THIS ARRAY'S ORDER. buildPaneColumn SORTS by
// at[0] (pane-wrapper.js), while ViewSelect renders this array VERBATIM. Swapping
@ -193,15 +193,15 @@ function paneElement(pane) {
// `lang` is the alias the page may carry; children read `locale`.
if (inherited.locale === undefined && inherited.lang !== undefined) inherited.locale = inherited.lang
const pcfg = { ...inherited, ...spec.value.config, ...pane.config }
// ★ NO "TAP A COLOURED DOT" COACH-MARK ON THIS PROFILE, AT ALL (Driver, 2026-09-27).
// ★ NO "TAP A COLOURED DOT" COACH-MARK ON THIS PROFILE, AT ALL (2026-09-27).
// ProfileLoader mounts PinHint by default and self-gates it on a map <canvas> existing, which is a
// sound gate for the question it asks ("is there a map here?") and the WRONG question for this
// profile. UPG-100's map is a POSTER PREVIEW: the reader is looking at what will be printed, and there
// is no per-dot detail to open. So the pill invites a gesture that does nothing, and on the phone the
// Driver's screenshots show it over a page with no coloured dots on it at all.
// reported screenshots show it over a page with no coloured dots on it at all.
// ⚠ THIS SUPERSEDES THE inert WORK FOR THIS PROFILE, and that is worth saying plainly rather than
// quietly leaving both in: PILLRISK was "the pill shows over the posters", and I fixed WHERE it shows.
// The Driver's answer is that it should not show HERE, full stop — a narrower and more correct claim
// The ruling is that it should not show HERE, full stop — a narrower and more correct claim
// than mine. The inert signal stays because it is still right for its own reasons (a stacked pane
// should not be in tab order or the a11y tree, and studio's PinHint change serves profiles that DO
// want the pill), but it is no longer what keeps this pill off this app.
@ -215,7 +215,7 @@ function paneElement(pane) {
// ★ TELL THE INVISIBLE PANE THAT IT IS INVISIBLE. `inert` on every pane except the active one.
//
// THE DEFECT, reported by the Driver and traced by repo (PILLRISK, 2026-09-27): PinHint's "Tap a
// THE DEFECT, reported from use and traced by repo (PILLRISK, 2026-09-27): PinHint's "Tap a
// coloured dot" pill appeared OVER the Prayer Resources posters on a phone. Its IntersectionObserver
// was working correctly — that is the point. The panes are `position:absolute; inset:0` in one
// scroll-snap column, so the map pane is STACKED BEHIND the active view at full size: 100% of its
@ -261,9 +261,9 @@ function go(id) {
//
// ★ SCROLLING WAS ONLY HALF THE HANDOFF, and the missing half was invisible because the
// visible part worked: go('map') moved the reader to the map, so the action looked like it
// had happened, while the map still showed all 27 zones. The Driver's brief for this is
// "when I click Show on map it should be basically the same as if I searched that result in
// the search bar" — i.e. ISOLATED, not merely scrolled to.
// had happened, while the map still showed all 27 zones. The requirement is that
// Show on map behaves as if that result had been searched in
// the search bar — i.e. ISOLATED, not merely scrolled to.
//
// The map half was already built and waiting: upg-100-list.vue listens for 'doxa:select-zone'
// ON ITS OWN ELEMENT (not on window, so several embeds never cross-talk) and routes it through
@ -305,7 +305,7 @@ onMounted(() => {
paneEls[pane.id] = el
column.mountEl(pane.id)?.appendChild(el)
}
// ★ THE FRESH-LOAD CASE, which is the one the Driver actually hit. Without this the panes carry no
// ★ THE FRESH-LOAD CASE, which is the one actually reported. Without this the panes carry no
// inert attribute at all until the reader touches the switcher, so the pill is over the posters on
// ARRIVAL — before any view change has happened. A signal that only fires on transitions never
// describes the initial state.
@ -357,7 +357,7 @@ useShadowStyles(PANE_GRID_STYLES + `
background:var(--ui-bg); color:var(--ui-text);
font:14px/1.4 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,system-ui,sans-serif;
}
/* NO AUTO-DARK. The DX design pattern is material LIGHT (Driver 2026-09-20): a system set to dark
/* NO AUTO-DARK. The DX design pattern is material LIGHT (decided 2026-09-20): a system set to dark
mode must NOT flip this element. The prefers-color-scheme:dark override that used to live here is
removed deliberately — the light palette above is the only theme. */
/* ── header: same height/border as the shell; switcher centred; share at right ── */