library/ keeps the reason and drops the speaker: no comment quotes or cites the Driver
Every reference to the Driver in library/ is reworded: the engineering reason stays, the speaker and the quote go, and a decision still open reads "provisional, pending a product decision". Comments only: each changed file is identical after stripping comments (JavaScript, template and CSS, including CSS comments inside the style strings shadow-DOM components inject). Those style-string comments do ship, so several bundles change by exactly those comment bytes.
This commit is contained in:
parent
474b0b4e8a
commit
bfc99ff827
59 changed files with 320 additions and 357 deletions
|
|
@ -92,7 +92,7 @@ const profileName = computed(() => config.value?.profile || null)
|
||||||
// prop is genuinely absent: profile-config arrives with keys profile/dataSource/instanceId/
|
// prop is genuinely absent: profile-config arrives with keys profile/dataSource/instanceId/
|
||||||
// assetsBase and NO tk. This computed then produced '' and provided it to every child, so a map
|
// assetsBase and NO tk. This computed then produced '' and provided it to every child, so a map
|
||||||
// nested INSIDE the composite got an empty accessToken and logged
|
// nested INSIDE the composite got an empty accessToken and logged
|
||||||
// "No Mapbox access token provided" — the error the Driver saw at dashboard-plus-grid.js:605.
|
// "No Mapbox access token provided" — the error reported at dashboard-plus-grid.js:605.
|
||||||
//
|
//
|
||||||
// The generator's assumption was that a composite's nested maps each render in their OWN IFRAME
|
// The generator's assumption was that a composite's nested maps each render in their OWN IFRAME
|
||||||
// LEAF running its own token ladder. That is false for dashboard-plus-grid: it has zero iframes
|
// LEAF running its own token ladder. That is false for dashboard-plus-grid: it has zero iframes
|
||||||
|
|
@ -123,8 +123,8 @@ const mapboxToken = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// SHARE WHAT THIS ELEMENT WAS GIVEN. An element handed a real `tk` publishes it page-wide, so a
|
// SHARE WHAT THIS ELEMENT WAS GIVEN. An element handed a real `tk` publishes it page-wide, so a
|
||||||
// SIBLING or NESTED element that was handed none stops starting from nothing — the Driver's point
|
// SIBLING or NESTED element that was handed none stops starting from nothing: a token belongs to
|
||||||
// that a token belongs to the page, not to whichever component happened to receive it. Publishing
|
// the page, not to whichever component happened to receive it. Publishing
|
||||||
// is idempotent and ignores empties, so this cannot clobber a good token with a blank one.
|
// is idempotent and ignores empties, so this cannot clobber a good token with a blank one.
|
||||||
watch(() => config.value?.tk, (tk) => { if (typeof tk === 'string' && tk) publishMapToken(tk) }, { immediate: true })
|
watch(() => config.value?.tk, (tk) => { if (typeof tk === 'string' && tk) publishMapToken(tk) }, { immediate: true })
|
||||||
// A token-SERVICE URL in the profile-config (tokenUrl) is asked only when no token is at hand; its answer
|
// A token-SERVICE URL in the profile-config (tokenUrl) is asked only when no token is at hand; its answer
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* WAGF region color strategy — colors pins by the region they belong to.
|
* WAGF region color strategy — colors pins by the region they belong to.
|
||||||
*
|
*
|
||||||
* ★ THE VALUES ARE THE RESEARCH MAP'S, ON THE DRIVER'S INSTRUCTION (2026-09-24). He compared the
|
* ★ THE VALUES ARE THE RESEARCH MAP'S (2026-09-24). The Simple Research map's Regions tab and the
|
||||||
* two surfaces himself: "when i look on teh color ffor th regiosn tab of the simpel research map i
|
* regions dashboard painted the same regions in different colours. Two surfaces disagreeing about
|
||||||
* don tsee the you using htes colors on the reigosn dashbarod hwen you shoudl". Two surfaces
|
* what Africa's colour means is a product defect whichever palette is prettier, and the research
|
||||||
* disagreeing about what Africa's colour means is a product defect whichever palette is prettier,
|
* map's palette was chosen to win. Source of truth:
|
||||||
* and he chose which one wins. Source of truth:
|
|
||||||
* library/colors/doxa-region.js (it lived at plugin-designs/doxa-research-map/src/colors/ until C1a), which the research dashboard and
|
* library/colors/doxa-region.js (it lived at plugin-designs/doxa-research-map/src/colors/ until C1a), which the research dashboard and
|
||||||
* three other consumers already paint from.
|
* three other consumers already paint from.
|
||||||
*
|
*
|
||||||
|
|
@ -25,10 +24,10 @@
|
||||||
* seven regions as a TABLE with names and figures. Identity never rests on hue alone on either.
|
* seven regions as a TABLE with names and figures. Identity never rests on hue alone on either.
|
||||||
* The LIGHTNESS FAIL is the part with no relief: three of these read faintly as PINS on a light
|
* The LIGHTNESS FAIL is the part with no relief: three of these read faintly as PINS on a light
|
||||||
* basemap. Darkening those three by ~10% L would enter the band while keeping the hue
|
* basemap. Darkening those three by ~10% L would enter the band while keeping the hue
|
||||||
* recognisable, and that is the Driver's call to make, not mine to make quietly — asked, not done.
|
* recognisable. That is provisional, pending a product decision — asked, not done quietly.
|
||||||
*
|
*
|
||||||
* WHY THIS EXISTS. The regions element's no-selection state is a CHOOSER: the Driver's brief was
|
* WHY THIS EXISTS. The regions element's no-selection state is a CHOOSER: a map that shows the
|
||||||
* "some type of map that would show the regions and countries and let you select one". For the
|
* regions and countries and lets you select one. For the
|
||||||
* seven regions to read AS regions the moment the map loads, the pins have to be colored by
|
* seven regions to read AS regions the moment the map loads, the pins have to be colored by
|
||||||
* region — which is a categorical encoding no existing strategy provides. (`_registry.js` and
|
* region — which is a categorical encoding no existing strategy provides. (`_registry.js` and
|
||||||
* `religion.js` both mention a `doxa-region.js`; it does not exist in this tree.)
|
* `religion.js` both mention a `doxa-region.js`; it does not exist in this tree.)
|
||||||
|
|
@ -66,7 +65,7 @@ export const PROPERTY_KEY = 'region'
|
||||||
// DX-life --color-brand-lighter: an unknown region reads as chrome, not as an eighth region.
|
// DX-life --color-brand-lighter: an unknown region reads as chrome, not as an eighth region.
|
||||||
// ★ DELIBERATELY NOT THEIRS. doxa-region.js uses '#0098FF' for null/undefined and keys
|
// ★ DELIBERATELY NOT THEIRS. doxa-region.js uses '#0098FF' for null/undefined and keys
|
||||||
// 'No WAGF Region/Bloc' to '#1a1a2e'. Neither is a REGION colour — they are what an absent value
|
// 'No WAGF Region/Bloc' to '#1a1a2e'. Neither is a REGION colour — they are what an absent value
|
||||||
// looks like — and the Driver's instruction was about the seven regions. A bright blue for
|
// looks like — and the instruction was about the seven regions. A bright blue for
|
||||||
// "unknown" would also read as a region on a map whose every other pin is a region. Every row in
|
// "unknown" would also read as a region on a map whose every other pin is a region. Every row in
|
||||||
// this feed carries a wagf_region, so this value is almost never painted; it is chrome on purpose.
|
// this feed carries a wagf_region, so this value is almost never painted; it is chrome on purpose.
|
||||||
const DEFAULT_COLOR = '#6d766e'
|
const DEFAULT_COLOR = '#6d766e'
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* ShareCalendar — the same 5,200-cell grid, shaded by a population-weighted share.
|
* ShareCalendar — the same 5,200-cell grid, shaded by a population-weighted share.
|
||||||
*
|
*
|
||||||
* ★ shareLabel IS REQUIRED AND HAS NO DEFAULT, on purpose. The shipped figure is the EVANGELICAL
|
* ★ shareLabel IS REQUIRED AND HAS NO DEFAULT, on purpose. The shipped figure is the EVANGELICAL
|
||||||
* share (christian-share.json: pctEvangelical 0.6133), and the brief and the Driver both say
|
* share (christian-share.json: pctEvangelical 0.6133), and the brief says
|
||||||
* "% Christian". IMB publishes no %-Christian column at all, so a true "% Christian" is not
|
* "% Christian". IMB publishes no %-Christian column at all, so a true "% Christian" is not
|
||||||
* computable from repo data (UPG100-RESEARCH-FINDINGS.md §3). This project has already retracted one
|
* computable from repo data (UPG100-RESEARCH-FINDINGS.md §3). This project has already retracted one
|
||||||
* P0 that was two metrics wearing one name. A default here would let this component be the thing
|
* P0 that was two metrics wearing one name. A default here would let this component be the thing
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@ export function scatterCells(total, n, seed = '') {
|
||||||
*
|
*
|
||||||
* `rows` is how many distinct grid rows the shaded cells touch. A contiguous block of 32 in a
|
* `rows` is how many distinct grid rows the shaded cells touch. A contiguous block of 32 in a
|
||||||
* 52-wide grid touches 1; a scatter of 32 over 5,200 touches close to 32. That single number is the
|
* 52-wide grid touches 1; a scatter of 32 over 5,200 touches close to 32. That single number is the
|
||||||
* difference between the page the Driver asked for and the one that shipped.
|
* difference between the page that was asked for and the one that shipped.
|
||||||
*/
|
*/
|
||||||
export function scatterSpread(picked, columns) {
|
export function scatterSpread(picked, columns) {
|
||||||
const cols = Number(columns) || 1
|
const cols = Number(columns) || 1
|
||||||
|
|
|
||||||
|
|
@ -90,8 +90,8 @@ function rowsExcluding(exclude) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SECTION COLLAPSE STATE (BUG-DASHBOARD-BAND-GROWS-ENORMOUS-WITH-LIVE-DATA) ────────────────
|
// ── SECTION COLLAPSE STATE (BUG-DASHBOARD-BAND-GROWS-ENORMOUS-WITH-LIVE-DATA) ────────────────
|
||||||
// Driver: "once the data fills all the sidebars, it causes the dashboard to become really tall...
|
// Once live data filled the sidebars, the dashboard grew very tall and every section had to be
|
||||||
// you have to like collapse all of the data." MEASURED live at 1440x900 in page-scroll mode
|
// collapsed by hand. MEASURED live at 1440x900 in page-scroll mode
|
||||||
// (dashboard-plus-grid): all five sections open by default rendered 69 rows total (16 People / 7
|
// (dashboard-plus-grid): all five sections open by default rendered 69 rows total (16 People / 7
|
||||||
// Place / 31 Language / 10 Religion / 5 Gospel Access), and the rail's OWN scroller never engaged
|
// Place / 31 Language / 10 Religion / 5 Gospel Access), and the rail's OWN scroller never engaged
|
||||||
// — frBody clientHeight === scrollHeight === 2117px — because a fully-open rail is simply that
|
// — frBody clientHeight === scrollHeight === 2117px — because a fully-open rail is simply that
|
||||||
|
|
@ -103,12 +103,9 @@ function rowsExcluding(exclude) {
|
||||||
// header, same interaction as before.
|
// header, same interaction as before.
|
||||||
// ★ OPEN BY DEFAULT — and that is NOT a revert to the bug, it is the correct reading of it.
|
// ★ OPEN BY DEFAULT — and that is NOT a revert to the bug, it is the correct reading of it.
|
||||||
//
|
//
|
||||||
// The previous pass set every section CLOSED, because the Driver said the rail "shouldn't be fully
|
// The previous pass set every section CLOSED, a literal reading of "the rail should not be fully
|
||||||
// expanded". That was a literal reading of a symptom report. His correction, verbatim: "when I told
|
// expanded". The intent was different: People, Place, Language and Religion stay OPEN, but open
|
||||||
// you they shouldn't be fully expanded, I didn't mean collapse all the sidebars. I want people,
|
// inside a bounded scroller, so they never grow the dashboard's height.
|
||||||
// place, language, religion EXPANDED. But when I say expanded I just mean OPEN — that doesn't mean
|
|
||||||
// fully expanded without a scroll bar. They should be open, but still have a scroll bar so they
|
|
||||||
// don't fully expand and cause the height of the dashboard to be super tall."
|
|
||||||
//
|
//
|
||||||
// So the defect was never "the sections are open". It was that opening them grew the PAGE instead of
|
// So the defect was never "the sections are open". It was that opening them grew the PAGE instead of
|
||||||
// filling a bounded scroller — 69 rows of content pushing the band to 2325px against a 900px
|
// filling a bounded scroller — 69 rows of content pushing the band to 2325px against a 900px
|
||||||
|
|
@ -170,7 +167,7 @@ function toRows(agg, facet, level) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── THE ROWS THAT WENT TO ZERO (card BUG-TREE-LEGEND-BUTTONS-GO-DEAD-AFTER-TOGGLING) ────────
|
// ── THE ROWS THAT WENT TO ZERO (card BUG-TREE-LEGEND-BUTTONS-GO-DEAD-AFTER-TOGGLING) ────────
|
||||||
// THE DRIVER'S REPORT: "after clicking around, the buttons started being unresponsive." The
|
// THE REPORT: after some clicking around, the buttons stopped responding. The
|
||||||
// buttons did not break — THEY LEFT. aggregate() only ever returns keys that survive the current
|
// buttons did not break — THEY LEFT. aggregate() only ever returns keys that survive the current
|
||||||
// filterSet, so a filtered-out sibling is simply ABSENT from the row list and is REMOVED FROM THE
|
// filterSet, so a filtered-out sibling is simply ABSENT from the row list and is REMOVED FROM THE
|
||||||
// DOM. Measured on the live composite (2026-09-22): selecting Place > Oceania took the rail from
|
// DOM. Measured on the live composite (2026-09-22): selecting Place > Oceania took the rail from
|
||||||
|
|
@ -192,10 +189,10 @@ function toRows(agg, facet, level) {
|
||||||
// unexpanded branch), which would ADD rows on filtering — the opposite surprise. The snapshot is
|
// unexpanded branch), which would ADD rows on filtering — the opposite surprise. The snapshot is
|
||||||
// keyed per section id and only ever taken while the dashboard is unfiltered.
|
// keyed per section id and only ever taken while the dashboard is unfiltered.
|
||||||
// ── WHICH LIST OWNS THE ORDER (card BUG-TREE-EXPANDS-AT-THE-BOTTOM-NOT-UNDER-THE-SELECTED-NODE) ──
|
// ── WHICH LIST OWNS THE ORDER (card BUG-TREE-EXPANDS-AT-THE-BOTTOM-NOT-UNDER-THE-SELECTED-NODE) ──
|
||||||
// THE DRIVER'S REPORT: "it expands the affinity block not underneath the selected affinity but
|
// THE REPORT: the affinity block expanded not underneath the selected affinity but underneath
|
||||||
// underneath the very final affinity group at the very bottom of the semantic tree legend." He
|
// the very last affinity group at the bottom of the semantic tree legend. The trigger was
|
||||||
// also isolated the trigger exactly: expanding with the caret ALONE is correct; SELECTING a row
|
// isolated exactly: expanding with the caret ALONE is correct; SELECTING a row and THEN expanding
|
||||||
// and THEN expanding it is broken.
|
// it is broken.
|
||||||
//
|
//
|
||||||
// ROOT CAUSE — this function used to let the BASELINE own the row order. It emitted every baseline
|
// ROOT CAUSE — this function used to let the BASELINE own the row order. It emitted every baseline
|
||||||
// row first (survivor or zero), and only then appended whatever the live list had that the baseline
|
// row first (survivor or zero), and only then appended whatever the live list had that the baseline
|
||||||
|
|
@ -301,8 +298,8 @@ function buildDrillRows(drill, topN) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── TREE TRUNKS (card BUILD-TREE-LEGEND-SEMANTIC-INDENTED) ────────────────────────────────
|
// ── TREE TRUNKS (card BUILD-TREE-LEGEND-SEMANTIC-INDENTED) ────────────────────────────────
|
||||||
// Driver: "it is very hard for me to read the content because I cannot see that it is INDENTED
|
// The content was hard to read because nothing showed it is INDENTED like a tree; the rail uses the
|
||||||
// like semantic trees. I want the semantic tree legend on the left just like on the research maps."
|
// same nested legend as the research maps.
|
||||||
//
|
//
|
||||||
// Root cause: the rail conveyed depth with padding-left ALONE (.l0/.l1/.l2). Whitespace does not
|
// Root cause: the rail conveyed depth with padding-left ALONE (.l0/.l1/.l2). Whitespace does not
|
||||||
// read as hierarchy — nothing visually ties a child row to its parent, so People > bloc > cluster
|
// read as hierarchy — nothing visually ties a child row to its parent, so People > bloc > cluster
|
||||||
|
|
@ -453,15 +450,15 @@ const scopeCount = computed(() => {
|
||||||
// expand work above rewrites `value`; the People axis's label swap writes `label` only, and the
|
// expand work above rewrites `value`; the People axis's label swap writes `label` only, and the
|
||||||
// 'Other' bucket renders a null data-facet so it can never be filtered on.
|
// 'Other' bucket renders a null data-facet so it can never be filtered on.
|
||||||
// ── AFFORDANCE (card BUG-SEMANTIC-TREE-SIDEBAR-NOT-INTEROPERABLE-WITH-GRID) ─────────────────
|
// ── AFFORDANCE (card BUG-SEMANTIC-TREE-SIDEBAR-NOT-INTEROPERABLE-WITH-GRID) ─────────────────
|
||||||
// DRIVER: "when I click on any of the people place language or religion semantic tree drop downs
|
// THE REPORT: clicking any of the people, place, language or religion semantic tree dropdowns
|
||||||
// nothing happens." Measured, that report was TRUE-AS-EXPERIENCED and the wiring was FINE: the
|
// did nothing. Measured, that report was TRUE-AS-EXPERIENCED and the wiring was FINE: the
|
||||||
// rail split its hit targets so that the caret NAVIGATED and only the row body FILTERED. Clicking
|
// rail split its hit targets so that the caret NAVIGATED and only the row body FILTERED. Clicking
|
||||||
// a caret grew the row count 66 -> 73 and correctly did not filter. Correct by design — and
|
// a caret grew the row count 66 -> 73 and correctly did not filter. Correct by design — and
|
||||||
// completely undiscoverable, because nothing on screen said the two halves of one row did two
|
// completely undiscoverable, because nothing on screen said the two halves of one row did two
|
||||||
// different things. A user must never click something that silently does nothing.
|
// different things. A user must never click something that silently does nothing.
|
||||||
//
|
//
|
||||||
// SO THE HIT TARGETS ARE NOW EXPLICIT, not positional:
|
// SO THE HIT TARGETS ARE NOW EXPLICIT, not positional:
|
||||||
// the WHOLE ROW filters — the Driver's mental model ("I clicked the row, select it")
|
// the WHOLE ROW filters — the reader's mental model: clicking a row selects it
|
||||||
// an explicit CHEVRON BUTTON expands — a real <button> with its own hit area, focus ring and
|
// an explicit CHEVRON BUTTON expands — a real <button> with its own hit area, focus ring and
|
||||||
// aria-label/aria-expanded, so "open this branch" is a thing you can
|
// aria-label/aria-expanded, so "open this branch" is a thing you can
|
||||||
// SEE and tab to rather than a 12px strip you must know about.
|
// SEE and tab to rather than a 12px strip you must know about.
|
||||||
|
|
@ -530,9 +527,8 @@ useShadowStyles(`
|
||||||
.rd-facets .fr-body::-webkit-scrollbar-thumb{ background:var(--dx-scroll-thumb, #c4c8c0); border-radius:8px; }
|
.rd-facets .fr-body::-webkit-scrollbar-thumb{ background:var(--dx-scroll-thumb, #c4c8c0); border-radius:8px; }
|
||||||
.rd-facets .fr-body::-webkit-scrollbar-thumb:hover{ background:#9aa197; }
|
.rd-facets .fr-body::-webkit-scrollbar-thumb:hover{ background:#9aa197; }
|
||||||
.fr-sec{ border-bottom:1px solid var(--rd-line, #d1d4cd); }
|
.fr-sec{ border-bottom:1px solid var(--rd-line, #d1d4cd); }
|
||||||
/* ★ STICKY SECTION HEADERS, GITHUB-MARKDOWN STYLE (Driver 2026-09-23: "when I am scrolling
|
/* ★ STICKY SECTION HEADERS, GITHUB-MARKDOWN STYLE: while scrolling, each section's header stays
|
||||||
down it needs to keep the section headers visible until you scroll into the next section --
|
visible until the next section scrolls in, as in a rendered markdown file. The conventional mechanism does all of it natively: position:sticky
|
||||||
like markdown files"). The conventional mechanism does all of it natively: position:sticky
|
|
||||||
top:0 inside the scrolling .fr-body pins the current header, and the NEXT header pushes the
|
top:0 inside the scrolling .fr-body pins the current header, and the NEXT header pushes the
|
||||||
previous one away as it arrives, because two sticky siblings at the same top contend for the
|
previous one away as it arrives, because two sticky siblings at the same top contend for the
|
||||||
slot. The opaque background is required -- rows scroll UNDER the pinned header, and sticky
|
slot. The opaque background is required -- rows scroll UNDER the pinned header, and sticky
|
||||||
|
|
@ -549,12 +545,11 @@ useShadowStyles(`
|
||||||
spacing that the gap used to provide is restored by margin-left on .fr-lbl below. */
|
spacing that the gap used to provide is restored by margin-left on .fr-lbl below. */
|
||||||
/* min-height replaces an implicit one-line height so a WRAPPED label (see .fr-lbl) grows the
|
/* min-height replaces an implicit one-line height so a WRAPPED label (see .fr-lbl) grows the
|
||||||
row instead of overflowing it; single-line rows keep their previous rhythm. */
|
row instead of overflowing it; single-line rows keep their previous rhythm. */
|
||||||
/* ★ TIGHTENED 2026-09-23 (Driver: "the spacing between the rows is too thick, they take up way
|
/* ★ TIGHTENED: the rows were too tall, so the per-row padding came down. Vertical padding 3px -> 1px and
|
||||||
too much height -- the padding should be decreased per row"). Vertical padding 3px -> 1px and
|
|
||||||
min-height 22px -> 18px, which at 4px saved per row is ~276px across the 69 rows the rail
|
min-height 22px -> 18px, which at 4px saved per row is ~276px across the 69 rows the rail
|
||||||
renders with its four sections open -- most of a viewport.
|
renders with its four sections open -- most of a viewport.
|
||||||
★ FONT-SIZE IS NOT A LEVER HERE and must not become one: the Driver explicitly rejected
|
★ FONT-SIZE IS NOT A LEVER HERE and must not become one: text too small to read is not an
|
||||||
shrinking it ("super tiny text that cannot be read"), and the note below at .fr-lbl records
|
option, and the note below at .fr-lbl records
|
||||||
that the width for the count came from the BAR and the gutters for the same reason. 12px
|
that the width for the count came from the BAR and the gutters for the same reason. 12px
|
||||||
stays; only the whitespace around it moves.
|
stays; only the whitespace around it moves.
|
||||||
min-height 18px still clears a 12px glyph at line-height 1.3 (15.6px), so this tightens the
|
min-height 18px still clears a 12px glyph at line-height 1.3 (15.6px), so this tightens the
|
||||||
|
|
@ -582,7 +577,7 @@ useShadowStyles(`
|
||||||
.fr-row.on .fr-trunk-elbow::before,
|
.fr-row.on .fr-trunk-elbow::before,
|
||||||
.fr-row.on .fr-trunk-elbow::after{ background:var(--rd-primary,#92b195); }
|
.fr-row.on .fr-trunk-elbow::after{ background:var(--rd-primary,#92b195); }
|
||||||
/* ── SELECTION EASES IN, IT DOES NOT SNAP (card BUG-TREE-SELECTION-SNAPS-NO-TRANSITION) ────
|
/* ── SELECTION EASES IN, IT DOES NOT SNAP (card BUG-TREE-SELECTION-SNAPS-NO-TRANSITION) ────
|
||||||
DRIVER: the tree selection "snaps" — it arrives in one frame with no sense of movement.
|
THE REPORT: the tree selection snaps — it arrives in one frame with no sense of movement.
|
||||||
MEASURED CAUSE, and it was NOT the one the card guessed. The card's lead was that
|
MEASURED CAUSE, and it was NOT the one the card guessed. The card's lead was that
|
||||||
keepZeroRows rebuilds the row array and Vue recreates the nodes, leaving a CSS transition
|
keepZeroRows rebuilds the row array and Vue recreates the nodes, leaving a CSS transition
|
||||||
with nothing to animate from. That is not what happens: the template keys each row on
|
with nothing to animate from. That is not what happens: the template keys each row on
|
||||||
|
|
@ -662,8 +657,8 @@ useShadowStyles(`
|
||||||
box-shadow:inset 0 0 0 1px rgba(59,70,61,.18); }
|
box-shadow:inset 0 0 0 1px rgba(59,70,61,.18); }
|
||||||
/* re-introduce the inter-column spacing the row's gap used to supply (trunks stay flush) */
|
/* re-introduce the inter-column spacing the row's gap used to supply (trunks stay flush) */
|
||||||
/* WRAP, DON'T ELLIPSISE — the COUNT is the information, and nowrap was hiding it.
|
/* WRAP, DON'T ELLIPSISE — the COUNT is the information, and nowrap was hiding it.
|
||||||
Driver-reported 2026-09-22: "North America and non-Spanish Caribbean is way too long... I
|
Reported: long labels such as "North America and non-Spanish Caribbean" and "Chinese folk
|
||||||
can't even scroll right to see the numbers", same for "Chinese folk religion". The old rule
|
religion" pushed the numbers out of reach, with no way to scroll right to them. The old rule
|
||||||
was white-space:nowrap + overflow:hidden + text-overflow:ellipsis, which let a long label
|
was white-space:nowrap + overflow:hidden + text-overflow:ellipsis, which let a long label
|
||||||
eat the row; because .fr-body is overflow-x:hidden (vertical scroll only, by design), there
|
eat the row; because .fr-body is overflow-x:hidden (vertical scroll only, by design), there
|
||||||
was NO way to scroll right and recover the number. An ellipsis hid the label AND cost the count.
|
was NO way to scroll right and recover the number. An ellipsis hid the label AND cost the count.
|
||||||
|
|
@ -672,11 +667,11 @@ useShadowStyles(`
|
||||||
ordinary labels still break at spaces. The trunk glyphs stay aligned because .fr-trunk is
|
ordinary labels still break at spaces. The trunk glyphs stay aligned because .fr-trunk is
|
||||||
align-self:stretch — it grows with the row instead of assuming one text line. */
|
align-self:stretch — it grows with the row instead of assuming one text line. */
|
||||||
/* ── ONE LINE PER ROW (card BUILD-TREE-LEGEND-ONE-LINE-ROWS-NARROWER-PROGRESS-BARS) ────────
|
/* ── ONE LINE PER ROW (card BUILD-TREE-LEGEND-ONE-LINE-ROWS-NARROWER-PROGRESS-BARS) ────────
|
||||||
DRIVER: the tree legend rows should each be ONE line, with narrower progress bars.
|
The tree legend rows should each be ONE line, with narrower progress bars.
|
||||||
THIS CARD AND THE WRAP NOTE ABOVE PULL IN OPPOSITE DIRECTIONS, and both Driver reports are
|
THIS CARD AND THE WRAP NOTE ABOVE PULL IN OPPOSITE DIRECTIONS, and both reports are real, so
|
||||||
real, so the resolution has to satisfy BOTH rather than simply reverting the earlier one:
|
the resolution has to satisfy BOTH rather than simply reverting the earlier one:
|
||||||
- the wrap rule exists because nowrap+ellipsis ATE THE COUNT ("I can't even scroll right
|
- the wrap rule exists because nowrap+ellipsis ATE THE COUNT (no way to scroll right to
|
||||||
to see the numbers"). Any return to a plain ellipsis re-breaks that, because .fr-body is
|
the numbers). Any return to a plain ellipsis re-breaks that, because .fr-body is
|
||||||
overflow-x:hidden by design and there is no way to scroll the number back into view.
|
overflow-x:hidden by design and there is no way to scroll the number back into view.
|
||||||
- this card wants the scannable one-line rhythm back.
|
- this card wants the scannable one-line rhythm back.
|
||||||
SO: clamp to ONE LINE, but buy the width for it instead of taking it out of the label.
|
SO: clamp to ONE LINE, but buy the width for it instead of taking it out of the label.
|
||||||
|
|
@ -686,8 +681,7 @@ useShadowStyles(`
|
||||||
the count can never be pushed out of the row. That is the specific failure being avoided:
|
the count can never be pushed out of the row. That is the specific failure being avoided:
|
||||||
the count stays on screen at every width, which was the whole point of the wrap fix.
|
the count stays on screen at every width, which was the whole point of the wrap fix.
|
||||||
The width to afford it comes from the BAR (34px -> 18px below) and the tightened gutters —
|
The width to afford it comes from the BAR (34px -> 18px below) and the tightened gutters —
|
||||||
NOT from the font, which the Driver explicitly rejected shrinking ("super tiny text that
|
NOT from the font, because text too small to read is not an option. overflow-wrap:anywhere is kept so a single unbreakable token still yields
|
||||||
cannot be read"). overflow-wrap:anywhere is kept so a single unbreakable token still yields
|
|
||||||
rather than overflowing the rail.
|
rather than overflowing the rail.
|
||||||
min-height on .fr-row stays: it is now an exact one-line height rather than a floor. */
|
min-height on .fr-row stays: it is now an exact one-line height rather than a floor. */
|
||||||
.fr-lbl{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis;
|
.fr-lbl{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis;
|
||||||
|
|
@ -697,20 +691,19 @@ useShadowStyles(`
|
||||||
were flex:1 1 auto and the bar's 24px floor competed with the text for the same row; with a
|
were flex:1 1 auto and the bar's 24px floor competed with the text for the same row; with a
|
||||||
wrapping label the bar must be the element that gives way, so it is capped and allowed to
|
wrapping label the bar must be the element that gives way, so it is capped and allowed to
|
||||||
shrink to zero-ish on a narrow rail. The COUNT (.fr-cnt, flex:0 0 auto) never shrinks — it is
|
shrink to zero-ish on a narrow rail. The COUNT (.fr-cnt, flex:0 0 auto) never shrinks — it is
|
||||||
the number the Driver could not previously see.
|
the number that could not previously be seen.
|
||||||
── 56px -> 34px (card BUILD-REBALANCE-THE-TWO-SIDEBARS) ──────────────────────────────────
|
── 56px -> 34px (card BUILD-REBALANCE-THE-TWO-SIDEBARS) ──────────────────────────────────
|
||||||
Driver: "the little progress bars need to be less wide... I am fine with smaller progress
|
The little progress bars needed to be narrower. Measured on the live composite before this change: the MEDIAN tree label had 85px
|
||||||
bars." Measured on the live composite before this change: the MEDIAN tree label had 85px
|
|
||||||
while the bar next to it had 56px — the bar was taking 40% of the label's budget to convey
|
while the bar next to it had 56px — the bar was taking 40% of the label's budget to convey
|
||||||
a magnitude that .fr-cnt already states EXACTLY, in numbers, on the same row. The bar is
|
a magnitude that .fr-cnt already states EXACTLY, in numbers, on the same row. The bar is
|
||||||
redundant precision; the label is the information. Combined with the wider rail this is
|
redundant precision; the label is the information. Combined with the wider rail this is
|
||||||
~50px handed back to every row's text, which is what drives the wrap count down.
|
~50px handed back to every row's text, which is what drives the wrap count down.
|
||||||
The bar is kept (not deleted) because the at-a-glance MAGNITUDE ranking is genuinely useful
|
The bar is kept (not deleted) because the at-a-glance MAGNITUDE ranking is genuinely useful
|
||||||
when scanning; 34px still resolves ~10 distinguishable steps, which is enough to rank rows.
|
when scanning; 34px still resolves ~10 distinguishable steps, which is enough to rank rows.
|
||||||
Do NOT solve a future width squeeze by shrinking the FONT — the Driver explicitly rejected
|
Do NOT solve a future width squeeze by shrinking the FONT: text too small to read is not an
|
||||||
"super tiny text that cannot be read". Shrink this bar further first.
|
option. Shrink this bar further first.
|
||||||
── 34px -> 18px, AND THE NOTE ABOVE IS THE REASON ────────────────────────────────────────
|
── 34px -> 18px, AND THE NOTE ABOVE IS THE REASON ────────────────────────────────────────
|
||||||
Driver, again, on this card: "narrower progress bars". The previous author left the exact
|
The same card asked again for narrower progress bars. The previous author left the exact
|
||||||
instruction for this situation one line up — when the rail needs width, the BAR yields
|
instruction for this situation one line up — when the rail needs width, the BAR yields
|
||||||
before the type does — so this takes it, rather than touching the font size.
|
before the type does — so this takes it, rather than touching the font size.
|
||||||
Those 16px per row are what pay for the one-line clamp on .fr-lbl: the label's budget goes
|
Those 16px per row are what pay for the one-line clamp on .fr-lbl: the label's budget goes
|
||||||
|
|
@ -738,9 +731,8 @@ useShadowStyles(`
|
||||||
<template>
|
<template>
|
||||||
<aside class="rd-facets" data-region="facets">
|
<aside class="rd-facets" data-region="facets">
|
||||||
<div class="fr-head">
|
<div class="fr-head">
|
||||||
<!-- "Vertical Legend" — the Driver's naming ruling (2026-09-23): "it shouldn't say tree
|
<!-- "Vertical Legend" — the naming rule: not "tree legend"; the component is Vertical, and
|
||||||
legend... rename the component to be called Vertical... use the term nesting or
|
the vocabulary is nesting or folders. The visible title says VERTICAL; the drill-down vocabulary in code and
|
||||||
folders". The visible title says VERTICAL; the drill-down vocabulary in code and
|
|
||||||
comments is NESTING/FOLDERS, not tree/semantic. Earlier ruling, kept: -->
|
comments is NESTING/FOLDERS, not tree/semantic. Earlier ruling, kept: -->
|
||||||
<!-- (prior note) the word "semantic" is dropped from the
|
<!-- (prior note) the word "semantic" is dropped from the
|
||||||
user-facing label (the component/docs still describe the semantic-tree MODEL).
|
user-facing label (the component/docs still describe the semantic-tree MODEL).
|
||||||
|
|
@ -769,8 +761,8 @@ useShadowStyles(`
|
||||||
research-map's SemanticTreeLegend so the hierarchy is DRAWN, not just padded. -->
|
research-map's SemanticTreeLegend so the hierarchy is DRAWN, not just padded. -->
|
||||||
<span v-for="(seg, i) in r.trunks" :key="'tr' + i" class="fr-trunk" :class="'fr-trunk-' + seg"></span>
|
<span v-for="(seg, i) in r.trunks" :key="'tr' + i" class="fr-trunk" :class="'fr-trunk-' + seg"></span>
|
||||||
<!-- CHEVRON = EXPAND, a real <button> with its own hit area, focus ring and label.
|
<!-- CHEVRON = EXPAND, a real <button> with its own hit area, focus ring and label.
|
||||||
Previously a 12px <span> that merely LOOKED like part of the row — the Driver
|
Previously a 12px <span> that merely LOOKED like part of the row — clicking it
|
||||||
clicked it expecting a selection and got navigation with no visible feedback.
|
expected a selection and got navigation with no visible feedback.
|
||||||
Leaf rows render an empty slot so every label in a tree stays left-aligned. -->
|
Leaf rows render an empty slot so every label in a tree stays left-aligned. -->
|
||||||
<button v-if="r.hasChildren" type="button" class="fr-caret-btn" :class="{ open: r.expanded }"
|
<button v-if="r.hasChildren" type="button" class="fr-caret-btn" :class="{ open: r.expanded }"
|
||||||
:data-expand="r.facet" :data-expand-value="r.value"
|
:data-expand="r.facet" :data-expand-value="r.value"
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ const removeLabel = (c) => T('aria.removeFilter', 'Remove filter {name}').replac
|
||||||
useShadowStyles(`
|
useShadowStyles(`
|
||||||
/* BOUNDED so the chips never slide under the centred search bar (BUG-SELECTION-CHIPS-UNDER-SEARCHBAR).
|
/* BOUNDED so the chips never slide under the centred search bar (BUG-SELECTION-CHIPS-UNDER-SEARCHBAR).
|
||||||
MEASURED live at 1440x900 before the fix: search x 540..900, first chip x 482..718 — ~178px of
|
MEASURED live at 1440x900 before the fix: search x 540..900, first chip x 482..718 — ~178px of
|
||||||
overlap, which is the Driver's "renders badly under the TOP search bar".
|
overlap, which is why the chips rendered badly under the top search bar.
|
||||||
WHY THE CHIPS MOVE AND NOT THE SEARCH: DashboardTopBar deliberately absolute-centres the search
|
WHY THE CHIPS MOVE AND NOT THE SEARCH: DashboardTopBar deliberately absolute-centres the search
|
||||||
(left:50% + translateX(-50%)) so its centre stays fixed "regardless of the left (title/breadcrumb/
|
(left:50% + translateX(-50%)) so its centre stays fixed "regardless of the left (title/breadcrumb/
|
||||||
chips) or right content widths" — a flow-based margin:auto would drift as chips grow. It also
|
chips) or right content widths" — a flow-based margin:auto would drift as chips grow. It also
|
||||||
|
|
@ -102,8 +102,8 @@ useShadowStyles(`
|
||||||
50% - min(180px, 21%). The chip strip starts after the brand/breadcrumb and must STOP SHORT of
|
50% - min(180px, 21%). The chip strip starts after the brand/breadcrumb and must STOP SHORT of
|
||||||
that edge. min(180px,21%) + a 14px gutter gives the max-width below; 34% is the conservative
|
that edge. min(180px,21%) + a 14px gutter gives the max-width below; 34% is the conservative
|
||||||
floor for narrow viewports where the 42% clamp binds instead of the 360px one.
|
floor for narrow viewports where the 42% clamp binds instead of the 360px one.
|
||||||
nowrap + the per-chip cap keeps the bar at 52px — the Driver has repeatedly said NEVER add a
|
nowrap + the per-chip cap keeps the bar at 52px. A second header row is ruled out, repeatedly
|
||||||
second header row, so growing the bar is not an option. Overflowing chips scroll horizontally
|
and firmly, so growing the bar is not an option. Overflowing chips scroll horizontally
|
||||||
and every truncated label keeps its full text in a title tooltip. */
|
and every truncated label keeps its full text in a title tooltip. */
|
||||||
/* NO max-width here ON PURPOSE. The bound lives ONCE, on .rd-left in DashboardTopBar (which wraps
|
/* NO max-width here ON PURPOSE. The bound lives ONCE, on .rd-left in DashboardTopBar (which wraps
|
||||||
brand + breadcrumb + chips together). I briefly had a second cap here and it was both redundant
|
brand + breadcrumb + chips together). I briefly had a second cap here and it was both redundant
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ const { t, te } = useOptionalI18n('', { fallback: (key) => PINHINT_FALLBACK[key]
|
||||||
const uiStore = inject('uiStore', null)
|
const uiStore = inject('uiStore', null)
|
||||||
const profileConfig = inject('profileConfig', null)
|
const profileConfig = inject('profileConfig', null)
|
||||||
const isDark = computed(() => uiStore?.theme === 'dark')
|
const isDark = computed(() => uiStore?.theme === 'dark')
|
||||||
// ★ THE DEFAULT MUST NOT NAME A COLOUR (Driver, reported 5+ times).
|
// ★ THE DEFAULT MUST NOT NAME A COLOUR (reported 5+ times).
|
||||||
// The old default was "Tap a red dot — someone still needs prayer". It shipped on EVERY map,
|
// The old default was "Tap a red dot — someone still needs prayer". It shipped on EVERY map,
|
||||||
// because `pinHintLabel` is an override no profile has ever set and `pinHint:false` is an opt-out
|
// because `pinHintLabel` is an override no profile has ever set and `pinHint:false` is an opt-out
|
||||||
// no profile has ever used — verified: zero occurrences of either across plugin-designs/.
|
// no profile has ever used — verified: zero occurrences of either across plugin-designs/.
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ useShadowStyles(`
|
||||||
clicks a pin and the detail panel swaps in over the language-family legend
|
clicks a pin and the detail panel swaps in over the language-family legend
|
||||||
(qa: 2026-05-02). */
|
(qa: 2026-05-02). */
|
||||||
.legend-container{position:absolute;top:60px;inset-inline-start:8px;width:var(--map-legend-width,380px);max-height:calc(100vh - 80px);background:#fff;border-radius:var(--map-radius,16px);box-shadow:0 2px 8px rgba(0,0,0,0.12),0 1px 3px rgba(0,0,0,0.08);z-index:1000;overflow:hidden;display:flex;flex-direction:column;transition:width 200ms cubic-bezier(0.2,0,0,1),max-height 200ms cubic-bezier(0.2,0,0,1),min-width 200ms cubic-bezier(0.2,0,0,1),border-radius 200ms linear,box-shadow 200ms linear;will-change:width,max-height;}
|
.legend-container{position:absolute;top:60px;inset-inline-start:8px;width:var(--map-legend-width,380px);max-height:calc(100vh - 80px);background:#fff;border-radius:var(--map-radius,16px);box-shadow:0 2px 8px rgba(0,0,0,0.12),0 1px 3px rgba(0,0,0,0.08);z-index:1000;overflow:hidden;display:flex;flex-direction:column;transition:width 200ms cubic-bezier(0.2,0,0,1),max-height 200ms cubic-bezier(0.2,0,0,1),min-width 200ms cubic-bezier(0.2,0,0,1),border-radius 200ms linear,box-shadow 200ms linear;will-change:width,max-height;}
|
||||||
/* ── DESKTOP TYPE SCALE for the LegendRows-based card (Driver, 2026-09-16: larger, more
|
/* ── DESKTOP TYPE SCALE for the LegendRows-based card (2026-09-16: larger, more
|
||||||
readable desktop legends). The semantic-tree panel's scale lives in SemanticTreeLegend.vue
|
readable desktop legends). The semantic-tree panel's scale lives in SemanticTreeLegend.vue
|
||||||
(it renders without this card on some maps). Size only, never layout. ── */
|
(it renders without this card on some maps). Size only, never layout. ── */
|
||||||
.legend-container .lrg-title{font-size:15px;}
|
.legend-container .lrg-title{font-size:15px;}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ useShadowStyles(`
|
||||||
nothing tappable in it; stated here, after the rule that would otherwise override it. */
|
nothing tappable in it; stated here, after the rule that would otherwise override it. */
|
||||||
.legend-sheet-root>.approx-pin-note--floating{pointer-events:none;}
|
.legend-sheet-root>.approx-pin-note--floating{pointer-events:none;}
|
||||||
|
|
||||||
/* ── Attribution stays visible (driver bug 2026-09-16) ──
|
/* ── Attribution stays visible (bug 2026-09-16) ──
|
||||||
The sheet is a bottom overlay, so the engine's bottom-left logo / bottom-right
|
The sheet is a bottom overlay, so the engine's bottom-left logo / bottom-right
|
||||||
attribution corners ended up UNDER it in every state (even the 48px collapsed
|
attribution corners ended up UNDER it in every state (even the 48px collapsed
|
||||||
footer fully covered the 19px logo). The sheet observes its own height
|
footer fully covered the 19px logo). The sheet observes its own height
|
||||||
|
|
@ -343,9 +343,8 @@ const props = defineProps({
|
||||||
/** Which tab the sheet OPENS on, 0-based. Forwarded to SemanticTreeLegend below.
|
/** Which tab the sheet OPENS on, 0-based. Forwarded to SemanticTreeLegend below.
|
||||||
*
|
*
|
||||||
* ★ WHY IT HAD TO BE ADDED. The desktop legend has taken :default-tab for a while, and
|
* ★ WHY IT HAD TO BE ADDED. The desktop legend has taken :default-tab for a while, and
|
||||||
* my-upg-100-list passes 1 (the UPG-100 Lists tab) because the Driver asked for exactly
|
* my-upg-100-list passes 1 (the UPG-100 Lists tab), because opening on Regions left the
|
||||||
* that — "it shouldn't have me sitting there on Regions, I don't know what to do, it's
|
* reader not knowing what to do. The mobile sheet had no such prop, so a phone opened on Region no matter
|
||||||
* confusing". The mobile sheet had no such prop, so a phone opened on Region no matter
|
|
||||||
* what the profile declared: the same complaint, unfixed, on the surface prayer
|
* what the profile declared: the same complaint, unfixed, on the surface prayer
|
||||||
* mobilisation most uses. Default 0 keeps every other consumer where it is. */
|
* mobilisation most uses. Default 0 keeps every other consumer where it is. */
|
||||||
defaultTab: {
|
defaultTab: {
|
||||||
|
|
@ -416,8 +415,8 @@ const onSemanticTreeSelect = inject('onSemanticTreeSelect', () => {})
|
||||||
// owns the Export menu — but passed neither :export-enabled nor @export. exportEnabled
|
// owns the Export menu — but passed neither :export-enabled nor @export. exportEnabled
|
||||||
// defaults to FALSE, so the control's v-if was never satisfied and a phone reader could
|
// defaults to FALSE, so the control's v-if was never satisfied and a phone reader could
|
||||||
// browse the UPG-100 lists and not export a packet or a poster. The packet IS the product
|
// browse the UPG-100 lists and not export a packet or a poster. The packet IS the product
|
||||||
// for that profile; the Driver asked for "print options at the top right of the legend...
|
// for that profile, so the print options sit at the top right of the legend: without them
|
||||||
// that's the only way that map would be usable".
|
// that map is not usable.
|
||||||
//
|
//
|
||||||
// ⚠ IT WAS NOT A DESIGN QUESTION, WHICH IS WHAT I FIRST RECORDED. The 0x0 elements that
|
// ⚠ IT WAS NOT A DESIGN QUESTION, WHICH IS WHAT I FIRST RECORDED. The 0x0 elements that
|
||||||
// measurement found belong to the OTHER, hidden SemanticTreeLegend instance — the desktop
|
// measurement found belong to the OTHER, hidden SemanticTreeLegend instance — the desktop
|
||||||
|
|
@ -626,7 +625,7 @@ const miniImageUrl = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── Touch resize — handlers live on the SHEET, not the 12px strip ──────────
|
// ── Touch resize — handlers live on the SHEET, not the 12px strip ──────────
|
||||||
// Driver bug 2026-09-16 ("moving the informational page up and down has bugs"),
|
// The bug: dragging the information sheet up and down misbehaved,
|
||||||
// reproduced by internal/mobile-sheet-probe.mjs. Three real causes, all fixed
|
// reproduced by internal/mobile-sheet-probe.mjs. Three real causes, all fixed
|
||||||
// here: (1) the only grab surface was the 12px strip, so a thumb on the title
|
// here: (1) the only grab surface was the 12px strip, so a thumb on the title
|
||||||
// row scrolled the content instead of resizing; (2) the 0.3s height transition
|
// row scrolled the content instead of resizing; (2) the 0.3s height transition
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
/**
|
/**
|
||||||
* LegendProgressBar — the prayer legend's header progress bar.
|
* LegendProgressBar — the prayer legend's header progress bar.
|
||||||
*
|
*
|
||||||
* WHY (Driver, 2026-09-16, README story): a global prayer movement needs to SEE
|
* WHY (the README story): a global prayer movement needs to SEE
|
||||||
* progress — how many people groups are covered in prayer out of the total, changing
|
* progress — how many people groups are covered in prayer out of the total, changing
|
||||||
* live as people sign up. ONE shared component so every prayer map (simple map
|
* live as people sign up. ONE shared component so every prayer map (simple map
|
||||||
* prayer tab, research map Prayer tab, and embeds) shows the same bar.
|
* prayer tab, research map Prayer tab, and embeds) shows the same bar.
|
||||||
*
|
*
|
||||||
* SPEC (Driver refinement, 2026-09-16 — keep this in sync with the behaviour):
|
* SPEC (refined 2026-09-16 — keep this in sync with the behaviour):
|
||||||
* 1. The bar lives in the HEADER of the prayer legend, and the prayer legend's
|
* 1. The bar lives in the HEADER of the prayer legend, and the prayer legend's
|
||||||
* title becomes "Progress" (desktop title bar and the mobile sheet title both —
|
* title becomes "Progress" (desktop title bar and the mobile sheet title both —
|
||||||
* via the shared components SemanticTreeLegend / LegendRows, never "Legend").
|
* via the shared components SemanticTreeLegend / LegendRows, never "Legend").
|
||||||
|
|
|
||||||
|
|
@ -249,7 +249,7 @@ defineEmits(['filter-click'])
|
||||||
// Progress payload: explicit prop wins, else the provided one (may be a ref/computed).
|
// Progress payload: explicit prop wins, else the provided one (may be a ref/computed).
|
||||||
const injectedProgress = inject('legendProgress', null)
|
const injectedProgress = inject('legendProgress', null)
|
||||||
const progressData = computed(() => props.progress ?? unref(injectedProgress) ?? null)
|
const progressData = computed(() => props.progress ?? unref(injectedProgress) ?? null)
|
||||||
// Driver 2026-09-16: the prayer legend's header says "Progress" (never "Legend").
|
// Since 2026-09-16 the prayer legend's header says "Progress" (never "Legend").
|
||||||
const displayTitle = computed(() => progressData.value ? t('legend.progress.title') : props.title)
|
const displayTitle = computed(() => progressData.value ? t('legend.progress.title') : props.title)
|
||||||
|
|
||||||
// Scroll-to-selected — universal across flat-legend tabs (prayer / engagement /
|
// Scroll-to-selected — universal across flat-legend tabs (prayer / engagement /
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,9 @@ function tabLabel(tab) {
|
||||||
// 2026-09-24 on the served upg-100-list leaf: ?lang=es changed 2 of 76 rendered strings
|
// 2026-09-24 on the served upg-100-list leaf: ?lang=es changed 2 of 76 rendered strings
|
||||||
// ("POPULATION" -> "POBLACIÓN") and left all three legend tabs AND all three info
|
// ("POPULATION" -> "POBLACIÓN") and left all three legend tabs AND all three info
|
||||||
// popovers in English — the locale switch works, the strings were simply never in a
|
// popovers in English — the locale switch works, the strings were simply never in a
|
||||||
// catalog. The Driver named this affordance specifically ("the tab itself gives you a
|
// catalog. This affordance was asked for specifically (the tab's own small information
|
||||||
// little information button that will explain what a UPG-100 list is"), which is exactly
|
// button explaining what a UPG-100 list is), which is exactly the sentence a reader who
|
||||||
// the sentence a reader who does not read English cannot use.
|
// does not read English cannot use.
|
||||||
//
|
//
|
||||||
// Same shape as tabLabel: `legend.tabInfo.<i18nKey|id>`, te() guard, falling back to the
|
// Same shape as tabLabel: `legend.tabInfo.<i18nKey|id>`, te() guard, falling back to the
|
||||||
// hardcoded English `info` so a tab with no catalog entry renders unchanged rather than
|
// hardcoded English `info` so a tab with no catalog entry renders unchanged rather than
|
||||||
|
|
@ -81,7 +81,7 @@ function tabInfo(tab) {
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
nodes: { type: Array, default: () => [] },
|
nodes: { type: Array, default: () => [] },
|
||||||
// APPROXPIN standing note (Driver card BUILD-APPROXIMATE-PIN-NOTICE: every map that shows people-group
|
// APPROXPIN standing note (card BUILD-APPROXIMATE-PIN-NOTICE: every map that shows people-group
|
||||||
// pins says plainly that a dot is a locality). null = decide from the map's own data: shown when the
|
// pins says plainly that a dot is a locality). null = decide from the map's own data: shown when the
|
||||||
// injected dataStore holds named Point features. true/false overrides, for a leaf that already says it.
|
// injected dataStore holds named Point features. true/false overrides, for a leaf that already says it.
|
||||||
pinNote: { type: Boolean, default: null },
|
pinNote: { type: Boolean, default: null },
|
||||||
|
|
@ -122,7 +122,7 @@ const instance = useInstance()
|
||||||
// Explicit prop wins, else the provided payload (a ref/computed from the profile).
|
// Explicit prop wins, else the provided payload (a ref/computed from the profile).
|
||||||
const injectedProgress = inject('legendProgress', null)
|
const injectedProgress = inject('legendProgress', null)
|
||||||
const progressData = computed(() => props.progress ?? unref(injectedProgress) ?? null)
|
const progressData = computed(() => props.progress ?? unref(injectedProgress) ?? null)
|
||||||
// Driver 2026-09-16: the prayer legend's header says "Progress" (never "Legend") —
|
// The prayer legend's header says "Progress" (never "Legend") —
|
||||||
// on the desktop title bar, the mobile sheet title and the collapsed reopen pill.
|
// on the desktop title bar, the mobile sheet title and the collapsed reopen pill.
|
||||||
const displayTitle = computed(() => progressData.value ? t('legend.progress.title') : props.title)
|
const displayTitle = computed(() => progressData.value ? t('legend.progress.title') : props.title)
|
||||||
const stlIsDark = computed(() => (instance?.theme.value || 'dark') === 'dark')
|
const stlIsDark = computed(() => (instance?.theme.value || 'dark') === 'dark')
|
||||||
|
|
@ -249,8 +249,8 @@ const sortDir = ref('desc') // 'desc' | 'asc'
|
||||||
// ⚠ THE STORE USED TO WIN UNCONDITIONALLY, which made `defaultTab` dead for every profile
|
// ⚠ THE STORE USED TO WIN UNCONDITIONALLY, which made `defaultTab` dead for every profile
|
||||||
// that shares an instance. instance.activeTab starts at 0 and 0 is indistinguishable from
|
// that shares an instance. instance.activeTab starts at 0 and 0 is indistinguishable from
|
||||||
// "the user chose tab 0", so a profile passing :default-tab="1" still opened on tab 0.
|
// "the user chose tab 0", so a profile passing :default-tab="1" still opened on tab 0.
|
||||||
// MEASURED 2026-09-24: my-upg-100-list opened on Region at desktop AND mobile, which is the
|
// MEASURED 2026-09-24: my-upg-100-list opened on Region at desktop AND mobile, which is exactly
|
||||||
// Driver's first complaint about this legend word for word.
|
// the first reported problem with this legend.
|
||||||
// Now the store only wins once something has ACTUALLY chosen a tab — a tab click, or a node
|
// Now the store only wins once something has ACTUALLY chosen a tab — a tab click, or a node
|
||||||
// selection that moves the legend to that node's generation (usePplrInstance sets the flag).
|
// selection that moves the legend to that node's generation (usePplrInstance sets the flag).
|
||||||
// Until then the profile's declared landing tab applies, which is what the prop always meant.
|
// Until then the profile's declared landing tab applies, which is what the prop always meant.
|
||||||
|
|
@ -294,8 +294,8 @@ watch(() => props.open, (v) => {
|
||||||
watch(() => safeNodes.value, () => {
|
watch(() => safeNodes.value, () => {
|
||||||
// ★ A DATA REFRESH IS NOT A DESELECTION (rega, WAGF Regions on Simple Research, 2026-09-28). The
|
// ★ A DATA REFRESH IS NOT A DESELECTION (rega, WAGF Regions on Simple Research, 2026-09-28). The
|
||||||
// polygon highlight landed 51-85 ms after the click and was wiped 433-667 ms later, leaving no row
|
// polygon highlight landed 51-85 ms after the click and was wiped 433-667 ms later, leaving no row
|
||||||
// marked and every region at full colour — which reads exactly like the Driver's "the legend isn't
|
// marked and every region at full colour — which reads exactly like a legend not connecting with
|
||||||
// connecting with the map", and is worse, because it connected and then undid itself.
|
// the map, and is worse, because it connected and then undid itself.
|
||||||
//
|
//
|
||||||
// This watcher fires on the IDENTITY of the nodes array, and every tree here is a computed that
|
// This watcher fires on the IDENTITY of the nodes array, and every tree here is a computed that
|
||||||
// returns a NEW array on each evaluation. So any late async that touches a dependency — pins still
|
// returns a NEW array on each evaluation. So any late async that touches a dependency — pins still
|
||||||
|
|
@ -663,8 +663,8 @@ useShadowStyles(`
|
||||||
/* --stl-top: the panel's offset from the map's top edge. The 60px default reserves room for a
|
/* --stl-top: the panel's offset from the map's top edge. The 60px default reserves room for a
|
||||||
search bar ABOVE the legend, which most consumers (research-map's geocoder) still have. A
|
search bar ABOVE the legend, which most consumers (research-map's geocoder) still have. A
|
||||||
profile that has REMOVED its search bar sets --stl-top on the legend's wrapper so the panel
|
profile that has REMOVED its search bar sets --stl-top on the legend's wrapper so the panel
|
||||||
rises into the vacated space instead of keeping a dead gap (Driver, 2026-09-23, UPG-100:
|
rises into the vacated space instead of keeping a dead gap (UPG-100 removed its search bar,
|
||||||
"the search bar is removed, that means the legend needs to be moved up"). The max-height
|
so its legend moves up). The max-height
|
||||||
mirrors it so a raised panel gains the same room at the bottom it gained at the top. */
|
mirrors it so a raised panel gains the same room at the bottom it gained at the top. */
|
||||||
position: absolute; inset-inline-start: 8px; top: var(--stl-top, 60px); width: var(--map-legend-width, 380px); z-index: 4;
|
position: absolute; inset-inline-start: 8px; top: var(--stl-top, 60px); width: var(--map-legend-width, 380px); z-index: 4;
|
||||||
display: flex; flex-direction: column;
|
display: flex; flex-direction: column;
|
||||||
|
|
@ -1022,7 +1022,7 @@ useShadowStyles(`
|
||||||
both, so they cannot drift apart again.
|
both, so they cannot drift apart again.
|
||||||
|
|
||||||
MEASURED 2026-09-24 at 390px in a 420px-tall host: the panel ran 38px past the bottom of the
|
MEASURED 2026-09-24 at 390px in a 420px-tall host: the panel ran 38px past the bottom of the
|
||||||
map. Driver: "the upg 100 list legend on the map is overflowing beyond the height of the map." */
|
map: the UPG-100 list's legend overflowed the map's height. */
|
||||||
.stl-panel { width: 92vw; max-width: 380px; --stl-top: 64px; }
|
.stl-panel { width: 92vw; max-width: 380px; --stl-top: 64px; }
|
||||||
.stl-reopen { top: 64px; }
|
.stl-reopen { top: 64px; }
|
||||||
}
|
}
|
||||||
|
|
@ -1163,8 +1163,8 @@ useShadowStyles(`
|
||||||
.stl-panel[data-theme="light"] .stl-export-item { color: #57606a; }
|
.stl-panel[data-theme="light"] .stl-export-item { color: #57606a; }
|
||||||
.stl-panel[data-theme="light"] .stl-export-lock { color: #57606a; }
|
.stl-panel[data-theme="light"] .stl-export-lock { color: #57606a; }
|
||||||
.stl-panel[data-theme="light"] .stl-export-soon { color: #57606a; background: rgba(208,215,222,0.5); }
|
.stl-panel[data-theme="light"] .stl-export-soon { color: #57606a; background: rgba(208,215,222,0.5); }
|
||||||
/* ── DESKTOP TYPE SCALE (Driver, 2026-09-16: "the desktop font sizes in the legends need to
|
/* ── DESKTOP TYPE SCALE: larger legend type on desktop, so the data reads more easily.
|
||||||
be increased … larger data to be more readable on desktop"). Desktop panel only — the
|
Desktop panel only — the
|
||||||
mobile sheet (.legend-mobile-sheet) keeps its own compact sizes. Size only, never layout;
|
mobile sheet (.legend-mobile-sheet) keeps its own compact sizes. Size only, never layout;
|
||||||
the base rules above stay as the mobile/default scale. ── */
|
the base rules above stay as the mobile/default scale. ── */
|
||||||
.stl-panel:not(.legend-mobile-sheet *) .stl-tb-title,
|
.stl-panel:not(.legend-mobile-sheet *) .stl-tb-title,
|
||||||
|
|
@ -1177,7 +1177,7 @@ useShadowStyles(`
|
||||||
.stl-panel:not(.legend-mobile-sheet *) .stl-num { font-size: 13px; }
|
.stl-panel:not(.legend-mobile-sheet *) .stl-num { font-size: 13px; }
|
||||||
/* The numeric columns must grow with the type: "POPULATION" at 10px is ~65px, ~76px with
|
/* The numeric columns must grow with the type: "POPULATION" at 10px is ~65px, ~76px with
|
||||||
its sort arrow; "139.0M" at 13.5px is ~49px. A fixed 44px column let the header collide
|
its sort arrow; "139.0M" at 13.5px is ~49px. A fixed 44px column let the header collide
|
||||||
with "UUPGs" and let values spill into the neighbouring column (Driver, 2026-09-16). */
|
with "UUPGs" and let values spill into the neighbouring column. */
|
||||||
.stl-panel:not(.legend-mobile-sheet *) .stl-inner { --stl-num-w: 70px; } /* the vars live on .stl-inner */
|
.stl-panel:not(.legend-mobile-sheet *) .stl-inner { --stl-num-w: 70px; } /* the vars live on .stl-inner */
|
||||||
.stl-panel:not(.legend-mobile-sheet *) .stl-hdr-num,
|
.stl-panel:not(.legend-mobile-sheet *) .stl-hdr-num,
|
||||||
.stl-panel:not(.legend-mobile-sheet *) .stl-num { white-space: nowrap; }
|
.stl-panel:not(.legend-mobile-sheet *) .stl-num { white-space: nowrap; }
|
||||||
|
|
|
||||||
|
|
@ -407,7 +407,7 @@ function injectGeocoderCSS(shadowRoot, prov) {
|
||||||
/**
|
/**
|
||||||
* Load a geocoder UMD bundle that the HOST PAGE did not preload.
|
* Load a geocoder UMD bundle that the HOST PAGE did not preload.
|
||||||
*
|
*
|
||||||
* ★ WHY MAPBOX NEEDS THIS TOO (Driver, 2026-09-23 — the search bar missing from a nested map).
|
* ★ WHY MAPBOX NEEDS THIS TOO (2026-09-23 — the search bar missing from a nested map).
|
||||||
* MapboxGeocoder was documented as "preloaded via index.html", and for a LEAF page that is true.
|
* MapboxGeocoder was documented as "preloaded via index.html", and for a LEAF page that is true.
|
||||||
* It is NOT true for a COMPOSITE leaf: generate-tree classifies that page as token-free and emits
|
* It is NOT true for a COMPOSITE leaf: generate-tree classifies that page as token-free and emits
|
||||||
* no geocoder script, while the composite goes on to nest a REAL map inside itself. MEASURED on the
|
* no geocoder script, while the composite goes on to nest a REAL map inside itself. MEASURED on the
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ const style = ref({})
|
||||||
// That is the identical failure the 2026-09-22 dashboard fix describes below, returning through a
|
// That is the identical failure the 2026-09-22 dashboard fix describes below, returning through a
|
||||||
// different door: not "a wrong side was chosen" but "nothing chose anything".
|
// different door: not "a wrong side was chosen" but "nothing chose anything".
|
||||||
// MEASURED 2026-09-28: cities-internet, gospel-resources, key-cities and upg-neighbor carry NO map-area
|
// MEASURED 2026-09-28: cities-internet, gospel-resources, key-cities and upg-neighbor carry NO map-area
|
||||||
// class and no data-map-area, so all four were unanchored. The Driver reported two of them; the list is
|
// class and no data-map-area, so all four were unanchored. Two of them were reported; the list is
|
||||||
// what made it four.
|
// what made it four.
|
||||||
const AREA = '.dsm-map-area, .rm-map-area, .dm-map-area, [data-map-area]'
|
const AREA = '.dsm-map-area, .rm-map-area, .dm-map-area, [data-map-area]'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ const props = defineProps({
|
||||||
/**
|
/**
|
||||||
* 'round' (default) keeps the separate floating circles every map ships today.
|
* 'round' (default) keeps the separate floating circles every map ships today.
|
||||||
* 'square' is the ENGINE'S OWN grouped card -- one white panel, 4px corners, a 2px ring and 1px
|
* 'square' is the ENGINE'S OWN grouped card -- one white panel, 4px corners, a 2px ring and 1px
|
||||||
* dividers -- which is the look the Driver asked for by name. Measured from
|
* dividers -- which is the look asked for by name. Measured from
|
||||||
* node_modules/maplibre-gl/dist/maplibre-gl.css, not invented:
|
* node_modules/maplibre-gl/dist/maplibre-gl.css, not invented:
|
||||||
* .maplibregl-ctrl-group { background:#fff; border-radius:4px }
|
* .maplibregl-ctrl-group { background:#fff; border-radius:4px }
|
||||||
* .maplibregl-ctrl-group:not(:empty) { box-shadow:0 0 0 2px rgba(0,0,0,.1) }
|
* .maplibregl-ctrl-group:not(:empty) { box-shadow:0 0 0 2px rgba(0,0,0,.1) }
|
||||||
|
|
@ -153,7 +153,7 @@ useShadowStyles(`
|
||||||
specificity, so the .map-toolbar gap:6px rule above is (0,1,0) exactly like the
|
specificity, so the .map-toolbar gap:6px rule above is (0,1,0) exactly like the
|
||||||
.map-toolbar--square gap:0 rule -- and it comes LATER in the sheet, so it won. On any phone a
|
.map-toolbar--square gap:0 rule -- and it comes LATER in the sheet, so it won. On any phone a
|
||||||
square toolbar would have fallen back into six separate buttons inside one ringed panel: the
|
square toolbar would have fallen back into six separate buttons inside one ringed panel: the
|
||||||
shape breaking precisely where the Driver is most likely to look at it.
|
shape breaking precisely where a reader is most likely to look at it.
|
||||||
The --no-geocoder pair is (0,2,0) and outranked the variant regardless of order, so it needs the
|
The --no-geocoder pair is (0,2,0) and outranked the variant regardless of order, so it needs the
|
||||||
two-class form to answer it. Same lesson as the goal cards' gap-vs-margin: a ported mechanism is
|
two-class form to answer it. Same lesson as the goal cards' gap-vs-margin: a ported mechanism is
|
||||||
only ported if it still WINS. */
|
only ported if it still WINS. */
|
||||||
|
|
|
||||||
|
|
@ -257,8 +257,8 @@ useShadowStyles(`
|
||||||
color:#a0aab8;
|
color:#a0aab8;
|
||||||
word-break:break-all;
|
word-break:break-all;
|
||||||
white-space:pre-wrap;
|
white-space:pre-wrap;
|
||||||
/* ★ NO SCROLLBAR — THE MENU SHOWS THE WHOLE SNIPPET (Driver 2026-09-23: "the share menu that
|
/* ★ NO SCROLLBAR — THE MENU SHOWS THE WHOLE SNIPPET (2026-09-23: the share menu opens fully
|
||||||
opens up should be fully expanded; it shouldn't have a scroll bar, but it currently has one").
|
expanded, with no scroll bar).
|
||||||
Was max-height:90px + overflow-y:auto, which clipped a ~4-line embed snippet to about two and
|
Was max-height:90px + overflow-y:auto, which clipped a ~4-line embed snippet to about two and
|
||||||
a half lines and put a scrollbar inside a popover. That is the wrong trade for THIS content:
|
a half lines and put a scrollbar inside a popover. That is the wrong trade for THIS content:
|
||||||
the box exists to be READ and COPIED in one glance, and user-select:all below means the whole
|
the box exists to be READ and COPIED in one glance, and user-select:all below means the whole
|
||||||
|
|
@ -435,9 +435,9 @@ async function copyEmbed() {
|
||||||
/* Anchoring OFF a map — the "menu opens off the right edge" fix */
|
/* Anchoring OFF a map — the "menu opens off the right edge" fix */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/*
|
/*
|
||||||
★ DRIVER-REPORTED TWICE (2026-09-22): "when you click the share button on the research
|
★ REPORTED TWICE (2026-09-22): on the research dashboard the share menu opened down and to the
|
||||||
dashboard, the menu is showing down to the RIGHT of the share button, and the share button is at
|
RIGHT of the share button, and the share button sits at the very top right of the page, so
|
||||||
the very top right of the page. So down-to-the-right is actually OFF the page."
|
down-to-the-right is OFF the page.
|
||||||
|
|
||||||
THE ROOT CAUSE IS AN ABSENT ANCHOR, NOT A WRONG ALIGNMENT CHOICE — measured, and it is a
|
THE ROOT CAUSE IS AN ABSENT ANCHOR, NOT A WRONG ALIGNMENT CHOICE — measured, and it is a
|
||||||
DIFFERENT bug from the FilterDropdown one it superficially resembles. That one was a
|
DIFFERENT bug from the FilterDropdown one it superficially resembles. That one was a
|
||||||
|
|
@ -459,7 +459,7 @@ async function copyEmbed() {
|
||||||
containing area. MapControlMenu's existing measured logic then does the whole job unchanged:
|
containing area. MapControlMenu's existing measured logic then does the whole job unchanged:
|
||||||
it compares the anchor's centre to the area's centre, opens on the INNER side, and clamps the
|
it compares the anchor's centre to the area's centre, opens on the INNER side, and clamps the
|
||||||
width to what fits. A share button at the top right is right-of-centre, so the menu opens to the
|
width to what fits. A share button at the top right is right-of-centre, so the menu opens to the
|
||||||
LEFT — which is what the Driver asked for, arrived at by measurement rather than by hardcoding a
|
LEFT — which is what was asked for, arrived at by measurement rather than by hardcoding a
|
||||||
side that would then be wrong for a left-hand placement.
|
side that would then be wrong for a left-hand placement.
|
||||||
|
|
||||||
WHY `inject` FIRST AND `provide` ONLY AS A FALLBACK: on a real map this component must keep
|
WHY `inject` FIRST AND `provide` ONLY AS A FALLBACK: on a real map this component must keep
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ const T = (key, en) => tStatic(key) || en;
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/**
|
/**
|
||||||
* What the Render button does. 'print' (default, Driver rule 2026-09-28: an export-poster button opens
|
* What the Render button does. 'print' (default, the rule since 2026-09-28: an export-poster button opens
|
||||||
* the PRINT DIALOG with a preview, never a direct download) prints the rendered poster in place; a PDF
|
* the PRINT DIALOG with a preview, never a direct download) prints the rendered poster in place; a PDF
|
||||||
* opens in the browser's viewer, where it can be printed or saved. 'download' saves the file, and a
|
* opens in the browser's viewer, where it can be printed or saved. 'download' saves the file, and a
|
||||||
* consumer that opts in must say why in a `posterAction-download:` comment (check:poster-action-print).
|
* consumer that opts in must say why in a `posterAction-download:` comment (check:poster-action-print).
|
||||||
|
|
|
||||||
|
|
@ -120,29 +120,28 @@ const props = defineProps({
|
||||||
// comment saying they "come from the renderer's own STOCK table so this list cannot
|
// comment saying they "come from the renderer's own STOCK table so this list cannot
|
||||||
// drift" — which was the intent, not the implementation. Deriving them makes it true.
|
// drift" — which was the intent, not the implementation. Deriving them makes it true.
|
||||||
cardStocks: { type: [Boolean, Array], default: false },
|
cardStocks: { type: [Boolean, Array], default: false },
|
||||||
// ★ DOWNLOAD OR PRINT? A DRIVER RULING, NOT A PREFERENCE — and the reason the
|
// ★ DOWNLOAD OR PRINT? A PRODUCT RULING, NOT A PREFERENCE — and the reason the
|
||||||
// 100-list could not simply adopt this component.
|
// 100-list could not simply adopt this component.
|
||||||
// Default 'download' is what country-posters and key-cities do today and keeps
|
// Default 'download' is what country-posters and key-cities do today and keeps
|
||||||
// them byte-for-byte unchanged. 'print' opens the OS print dialog on the poster
|
// them byte-for-byte unchanged. 'print' opens the OS print dialog on the poster
|
||||||
// instead, which is what my-upg-100-list/poster-wall.vue:280 records as the
|
// instead, which is what my-upg-100-list/poster-wall.vue:280 records as the
|
||||||
// Driver's explicit brief (2026-09-23): "three print buttons, each opening an
|
// brief: three print buttons, each opening an in-browser print dialog, and no
|
||||||
// in-browser print dialog, no silent file download as the default action. A raw
|
// silent file download as the default action (a raw 'Download' button and a
|
||||||
// 'Download' button and a 'Prayer List: coming soon' placeholder were exactly the
|
// 'Prayer List: coming soon' placeholder were the two rejected). Printing to PDF
|
||||||
// two buttons he objected to." Printing to PDF from that dialog covers the
|
// from that dialog covers the get-a-file need without a fourth competing button.
|
||||||
// get-a-file need without a fourth competing button.
|
|
||||||
// Migrating the 100-list onto this component WITHOUT this prop would have
|
// Migrating the 100-list onto this component WITHOUT this prop would have
|
||||||
// reintroduced both rejected buttons — the capability gap was never the whole
|
// reintroduced both rejected buttons — the capability gap was never the whole
|
||||||
// blocker; the shared component still encoded the pre-brief design.
|
// blocker; the shared component still encoded the pre-brief design.
|
||||||
// ★ DEFAULT 'print' (Driver rule 2026-09-28: an export-poster button opens the PRINT DIALOG with a
|
// ★ DEFAULT 'print': an export-poster button opens the PRINT DIALOG with a preview, never a direct
|
||||||
// preview, never a direct download). country-posters and key-cities set nothing and downloaded.
|
// download. country-posters and key-cities set nothing and downloaded.
|
||||||
// 'download' is an explicit opt-in, and a consumer that opts in must say why in a comment carrying
|
// 'download' is an explicit opt-in, and a consumer that opts in must say why in a comment carrying
|
||||||
// `posterAction-download:` (check:poster-action-print). Multi-page PDF packets and CSV exports are not
|
// `posterAction-download:` (check:poster-action-print). Multi-page PDF packets and CSV exports are not
|
||||||
// poster buttons and stay downloads.
|
// poster buttons and stay downloads.
|
||||||
posterAction: { type: String, default: 'print' }, // 'print' | 'download'
|
posterAction: { type: String, default: 'print' }, // 'print' | 'download'
|
||||||
// ★ IS THE PREVIEW ITSELF A CONTROL? Default 'none' keeps the image inert, which is
|
// ★ IS THE PREVIEW ITSELF A CONTROL? Default 'none' keeps the image inert, which is
|
||||||
// what country-posters and key-cities render today. 'print' makes clicking the
|
// what country-posters and key-cities render today. 'print' makes clicking the
|
||||||
// poster preview open it for printing — a Driver-named behaviour ("show the poster
|
// poster preview open it for printing (the poster shown in the browser to help
|
||||||
// in the browser to help print it"), and the LAST capability the my-upg-100-list
|
// print it), and the LAST capability the my-upg-100-list
|
||||||
// migration had not carried across.
|
// migration had not carried across.
|
||||||
// ⚠ WHEN IT IS A CONTROL IT MUST BE A KEYBOARD CONTROL. The original carried
|
// ⚠ WHEN IT IS A CONTROL IT MUST BE A KEYBOARD CONTROL. The original carried
|
||||||
// role="button", tabindex="0" and Enter/Space handlers alongside the click; an
|
// role="button", tabindex="0" and Enter/Space handlers alongside the click; an
|
||||||
|
|
@ -153,7 +152,7 @@ const props = defineProps({
|
||||||
previewAction: { type: String, default: 'none' }, // 'none' | 'print'
|
previewAction: { type: String, default: 'none' }, // 'none' | 'print'
|
||||||
// Hide the Prayer List button entirely, rather than showing it as a
|
// Hide the Prayer List button entirely, rather than showing it as a
|
||||||
// "coming soon" placeholder. The placeholder is honest where the feature is
|
// "coming soon" placeholder. The placeholder is honest where the feature is
|
||||||
// genuinely unbuilt, and it is the OTHER button the Driver objected to on a wall
|
// genuinely unbuilt, and it is the OTHER rejected button on a wall
|
||||||
// where the list DOES print — so a wall with a records provider can suppress it.
|
// where the list DOES print — so a wall with a records provider can suppress it.
|
||||||
hidePrayerListWhenSoon: { type: Boolean, default: false },
|
hidePrayerListWhenSoon: { type: Boolean, default: false },
|
||||||
// EMBEDDED mode (added 2026-09-24 for the regions-tab poster dropdown). The wall's root is
|
// EMBEDDED mode (added 2026-09-24 for the regions-tab poster dropdown). The wall's root is
|
||||||
|
|
@ -168,8 +167,8 @@ const props = defineProps({
|
||||||
// group in its own chrome (the region card's header and summary) would otherwise repeat it,
|
// group in its own chrome (the region card's header and summary) would otherwise repeat it,
|
||||||
// and with a single unnamed group the head renders the literal "Other". Default true.
|
// and with a single unnamed group the head renders the literal "Other". Default true.
|
||||||
sectionHeads:{ type: Boolean, default: true },
|
sectionHeads:{ type: Boolean, default: true },
|
||||||
// ★ HOW MANY POSTERS SIT IN A ROW. The Driver, on the regions posters: "i need two posters per
|
// ★ HOW MANY POSTERS SIT IN A ROW. The regions posters want two per row, with a setting to raise
|
||||||
// row on the regiosn posters and witha setitn got icnreat eh number of poswers per row".
|
// the number.
|
||||||
// 0 keeps the wall's existing auto-fill (repeat(auto-fill, minmax(210px, 1fr))), so the two
|
// 0 keeps the wall's existing auto-fill (repeat(auto-fill, minmax(210px, 1fr))), so the two
|
||||||
// panes that already ship — country-posters and my-upg-100-list — are byte-identical unless
|
// panes that already ship — country-posters and my-upg-100-list — are byte-identical unless
|
||||||
// their owner opts in. A positive number fixes the track count instead.
|
// their owner opts in. A positive number fixes the track count instead.
|
||||||
|
|
@ -192,9 +191,8 @@ watch(() => props.columns, (n) => { if (readStoredPerRow() == null) perRow.value
|
||||||
watch(perRow, (n) => {
|
watch(perRow, (n) => {
|
||||||
if (n === PER_ROW_AUTO || (Number.isFinite(n) && n >= PER_ROW_MIN && n <= PER_ROW_MAX)) storePerRow(n)
|
if (n === PER_ROW_AUTO || (Number.isFinite(n) && n >= PER_ROW_MIN && n <= PER_ROW_MAX)) storePerRow(n)
|
||||||
})
|
})
|
||||||
// The choices the control offers. Two is the Driver's default; the ceiling is the wall's own
|
// The choices the control offers: a minimum of 2, a maximum of 5, a default of 2; the ceiling is the
|
||||||
// auto-fill behaviour, offered as "fit" so the knob can always get back to where it started.
|
// wall's own auto-fill behaviour, offered as "fit" so the knob can always get back to where it started.
|
||||||
// Driver: "poster per row should have a minimum of 2 and a max of 5 and a default of 2".
|
|
||||||
// ★ AUTO IS 0, AND 0 IS NOT A COUNT — it is the absence of an override. gridStyle already returns
|
// ★ AUTO IS 0, AND 0 IS NOT A COUNT — it is the absence of an override. gridStyle already returns
|
||||||
// {} for a non-positive value, leaving the stylesheet's auto-fill rule in charge, so Auto needed no
|
// {} for a non-positive value, leaving the stylesheet's auto-fill rule in charge, so Auto needed no
|
||||||
// new branch in the layout: it needed to become REACHABLE from the control and from storage.
|
// new branch in the layout: it needed to become REACHABLE from the control and from storage.
|
||||||
|
|
@ -203,9 +201,8 @@ watch(perRow, (n) => {
|
||||||
const PER_ROW_AUTO = 0
|
const PER_ROW_AUTO = 0
|
||||||
const PER_ROW_CHOICES = [PER_ROW_AUTO, 2, 3, 4, 5]
|
const PER_ROW_CHOICES = [PER_ROW_AUTO, 2, 3, 4, 5]
|
||||||
const PER_ROW_MIN = 2, PER_ROW_MAX = 5, PER_ROW_DEFAULT = 2
|
const PER_ROW_MIN = 2, PER_ROW_MAX = 5, PER_ROW_DEFAULT = 2
|
||||||
// ...and "it needs to honour local storage so if you change the default number it is saved in local
|
// The choice persists in localStorage, so a number the reader sets survives a reload. One key for the
|
||||||
// storage so you can reload and your default you set is persistent". One key for the whole wall,
|
// whole wall, not per instance: the reader is setting a PREFERENCE, and a preference that only survives on the
|
||||||
// not per instance: the Driver is setting a PREFERENCE, and a preference that only survives on the
|
|
||||||
// page you set it on is not one. Every access is wrapped because localStorage THROWS outright in a
|
// page you set it on is not one. Every access is wrapped because localStorage THROWS outright in a
|
||||||
// private window or with site data blocked -- a preference must never be able to break the wall.
|
// private window or with site data blocked -- a preference must never be able to break the wall.
|
||||||
const PER_ROW_KEY = 'doxa-maps:poster-wall:per-row'
|
const PER_ROW_KEY = 'doxa-maps:poster-wall:per-row'
|
||||||
|
|
@ -232,7 +229,7 @@ function storePerRow(n) {
|
||||||
// An inline style beats the stylesheet, so `repeat(2, minmax(0,1fr))` overrode the CSS rule's
|
// An inline style beats the stylesheet, so `repeat(2, minmax(0,1fr))` overrode the CSS rule's
|
||||||
// `minmax(210px,1fr)` at EVERY width — including 390px, where two columns give a ~162px card.
|
// `minmax(210px,1fr)` at EVERY width — including 390px, where two columns give a ~162px card.
|
||||||
// Reported from a 390x844 phone shot: two posters per row, where the CSS alone would have
|
// Reported from a 390x844 phone shot: two posters per row, where the CSS alone would have
|
||||||
// given one. The stored preference is the Driver's and must survive; it is a preference for
|
// given one. The stored preference is the reader's and must survive; it is a preference for
|
||||||
// DESKTOP density, not an instruction to overflow a phone.
|
// DESKTOP density, not an instruction to overflow a phone.
|
||||||
// 210px is the same minimum the stylesheet's auto-fill uses, so the clamp agrees with the rule
|
// 210px is the same minimum the stylesheet's auto-fill uses, so the clamp agrees with the rule
|
||||||
// it is overriding rather than inventing a second breakpoint.
|
// it is overriding rather than inventing a second breakpoint.
|
||||||
|
|
@ -261,7 +258,7 @@ const itemCount = (n) => (props.itemCountKey ? countOf(props.itemCountKey, n) :
|
||||||
const base = computed(() => String(props.assetsBase || './assets').replace(/\/+$/, ''))
|
const base = computed(() => String(props.assetsBase || './assets').replace(/\/+$/, ''))
|
||||||
|
|
||||||
// ★ A SECOND PLACE TO LOOK, because "poster not generated yet" was being shown for posters that
|
// ★ A SECOND PLACE TO LOOK, because "poster not generated yet" was being shown for posters that
|
||||||
// ARE generated. The DRIVER reported it on Ethiopia, whose country-eth.webp exists (174 of the 175
|
// ARE generated. It was reported on Ethiopia, whose country-eth.webp exists (174 of the 175
|
||||||
// records have their .webp; India is the one that legitimately does not and carries its own
|
// records have their .webp; India is the one that legitimately does not and carries its own
|
||||||
// reason). MEASURED here — static build AND dev server, every request 200, zero cards in that
|
// reason). MEASURED here — static build AND dev server, every request 200, zero cards in that
|
||||||
// branch — so the file is not missing; a HOST LAYOUT is. resolveAssetsBase points at
|
// branch — so the file is not missing; a HOST LAYOUT is. resolveAssetsBase points at
|
||||||
|
|
@ -519,14 +516,13 @@ const previewIsControl = computed(() => props.previewAction === 'print')
|
||||||
// Which card currently has its size row revealed, by item id. Holding an ID
|
// Which card currently has its size row revealed, by item id. Holding an ID
|
||||||
// rather than a boolean-per-card means opening one CLOSES the others for free:
|
// rather than a boolean-per-card means opening one CLOSES the others for free:
|
||||||
// two size rows open at once would overlap in the grid gap, and there is no
|
// two size rows open at once would overlap in the grid gap, and there is no
|
||||||
// reading of the Driver's request where two are wanted.
|
// reading of the request where two are wanted.
|
||||||
// ★ NOW A COMPOUND KEY, `${id}:${which}` — the Driver, 2026-09-28, asked for the SAME caret on the
|
// ★ NOW A COMPOUND KEY, `${id}:${which}` — the Prayer List gets the SAME caret, with two entries
|
||||||
// Prayer List with two entries (Full detail / Lightweight), and explicitly not a merged menu and not
|
// (Full detail / Lightweight), and explicitly not a merged menu and not a second dropdown component. There is no dropdown COMPONENT here to reuse: this is an inline
|
||||||
// a second dropdown component. There is no dropdown COMPONENT here to reuse: this is an inline
|
|
||||||
// disclosure, one ref plus a sibling tape of plain buttons. So the PATTERN is generalized instead of
|
// disclosure, one ref plus a sibling tape of plain buttons. So the PATTERN is generalized instead of
|
||||||
// copied — copy/paste would give two independent open-states, and then opening the Prayer List would
|
// copied — copy/paste would give two independent open-states, and then opening the Prayer List would
|
||||||
// NOT close the Cards, so two tapes could sit open at once and overlap in the grid gap. That is a
|
// NOT close the Cards, so two tapes could sit open at once and overlap in the grid gap. That is a
|
||||||
// visible regression on the Cards row the Driver said to leave exactly as it is.
|
// visible regression on the Cards row, which is to stay exactly as it is.
|
||||||
// Holding ONE key keeps "opening one closes the others" free, which is why it was an id to begin with.
|
// Holding ONE key keeps "opening one closes the others" free, which is why it was an id to begin with.
|
||||||
const deckOpenFor = ref(null)
|
const deckOpenFor = ref(null)
|
||||||
const deckKey = (id, which) => `${id}:${which}`
|
const deckKey = (id, which) => `${id}:${which}`
|
||||||
|
|
@ -579,8 +575,8 @@ const packetVariants = [
|
||||||
// entry is picked. Opening a variant closes the row, like printDeckAt.
|
// entry is picked. Opening a variant closes the row, like printDeckAt.
|
||||||
// ★ THIS CALLS printPacket, NOT printList — and that difference is a DEFECT FIX, not a refactor.
|
// ★ THIS CALLS printPacket, NOT printList — and that difference is a DEFECT FIX, not a refactor.
|
||||||
// MEASURED: downloadPacket() above calls prints.printList(item), which renders a COLUMNAR TABLE
|
// MEASURED: downloadPacket() above calls prints.printList(item), which renders a COLUMNAR TABLE
|
||||||
// (name / country / population / religion — usePosterPrints.js:150). The PRAYER PACKET the Driver
|
// (name / country / population / religion — usePosterPrints.js:150). The PRAYER PACKET as specified
|
||||||
// specified — map, life calendar, %-evangelical grid, triangle, profiles, wheel, points — is
|
// — map, life calendar, %-evangelical grid, triangle, profiles, wheel, points — is
|
||||||
// prints.printPacket, and this wall never called it. Every OTHER surface does
|
// prints.printPacket, and this wall never called it. Every OTHER surface does
|
||||||
// (upg-100-list.vue:1430 and :1671, research-map.vue:1597, countries-map.vue:1360); the poster wall
|
// (upg-100-list.vue:1430 and :1671, research-map.vue:1597, countries-map.vue:1360); the poster wall
|
||||||
// was the sole exception. So the button labelled "Prayer List" here has been printing a list, which
|
// was the sole exception. So the button labelled "Prayer List" here has been printing a list, which
|
||||||
|
|
@ -741,9 +737,9 @@ useShadowStyles(
|
||||||
'.is-dark .tape{background:#1d2b23;color:#cfe9d9;border-color:rgba(255,255,255,.10)}' +
|
'.is-dark .tape{background:#1d2b23;color:#cfe9d9;border-color:rgba(255,255,255,.10)}' +
|
||||||
// ★ THE HEADER STICKER STRADDLES THE BANNER'S TOP EDGE, for the same reason the footer one
|
// ★ THE HEADER STICKER STRADDLES THE BANNER'S TOP EDGE, for the same reason the footer one
|
||||||
// straddles the bottom. MEASURED before: 35px above the banner's top edge, -1px below — it
|
// straddles the bottom. MEASURED before: 35px above the banner's top edge, -1px below — it
|
||||||
// floated over the card, touching the banner nowhere. The Driver reads the rounded top
|
// floated over the card, touching the banner nowhere. The rounded top sticker is the one
|
||||||
// sticker as the one that "looks beautiful and is really well spaced", so this keeps its
|
// that reads as well spaced, so this keeps its size and shape exactly and only corrects
|
||||||
// size and shape exactly and only corrects WHERE it sits: centred on the edge it is taping.
|
// WHERE it sits: centred on the edge it is taping.
|
||||||
// --banner-pad-top reserves the banner surface it lands on, so it never reaches the map.
|
// --banner-pad-top reserves the banner surface it lands on, so it never reaches the map.
|
||||||
// .tape-h is positioned against .card (it is a direct child), and the banner's top is the
|
// .tape-h is positioned against .card (it is a direct child), and the banner's top is the
|
||||||
// card's padding-top — hence top:24px, read from the card rather than restated.
|
// card's padding-top — hence top:24px, read from the card rather than restated.
|
||||||
|
|
@ -766,7 +762,7 @@ useShadowStyles(
|
||||||
// four controls (Prayer List · Card Deck · stock select · Poster Map) burst the box regardless.
|
// four controls (Prayer List · Card Deck · stock select · Poster Map) burst the box regardless.
|
||||||
// MEASURED 2026-09-24 at 1440px: all 27 cards overflowing, worst 110px past a 212px card.
|
// MEASURED 2026-09-24 at 1440px: all 27 cards overflowing, worst 110px past a 212px card.
|
||||||
//
|
//
|
||||||
// ★ flex-wrap:wrap FIXED THAT AND BROKE THE OTHER AXIS — driver-reported, same day. The row is
|
// ★ flex-wrap:wrap FIXED THAT AND BROKE THE OTHER AXIS — reported the same day. The row is
|
||||||
// absolutely positioned at bottom:-14px, so wrapping it to three lines grew it 35px -> 78px and
|
// absolutely positioned at bottom:-14px, so wrapping it to three lines grew it 35px -> 78px and
|
||||||
// pushed its TOP up from 216px to 173px into a 238px card: straight over the preview image and
|
// pushed its TOP up from 216px to 173px into a 238px card: straight over the preview image and
|
||||||
// the card's outline. Horizontal overflow traded for vertical. min-width:0 alone lets the
|
// the card's outline. Horizontal overflow traded for vertical. min-width:0 alone lets the
|
||||||
|
|
@ -774,13 +770,12 @@ useShadowStyles(
|
||||||
// The gate that passed this only measured horizontal edges; it now measures the row's top too.
|
// The gate that passed this only measured horizontal edges; it now measures the row's top too.
|
||||||
// ⚠ ANCHORED AT THE TOP, NOT THE BOTTOM. This was bottom:-14px, so any growth pushed the row
|
// ⚠ ANCHORED AT THE TOP, NOT THE BOTTOM. This was bottom:-14px, so any growth pushed the row
|
||||||
// UPWARD over the preview — which is exactly how wrapping it turned a sideways overflow into
|
// UPWARD over the preview — which is exactly how wrapping it turned a sideways overflow into
|
||||||
// a vertical one the driver saw. Anchoring at top:100% pins it just under the card, so the
|
// a vertical one. Anchoring at top:100% pins it just under the card, so the
|
||||||
// second line grows DOWN into empty space. MEASURED: 92px between the preview's bottom and
|
// second line grows DOWN into empty space. MEASURED: 92px between the preview's bottom and
|
||||||
// the next card, of which the single-line row used 35 — 51px spare.
|
// the next card, of which the single-line row used 35 — 51px spare.
|
||||||
// ★ THE FOOTER STICKER STRADDLES THE BANNER'S BOTTOM EDGE (Driver): "see how the header for
|
// ★ THE FOOTER STICKER STRADDLES THE BANNER'S BOTTOM EDGE. The header (Show on map) sits on the
|
||||||
// show on map is inline on the top of the poster box? but the footer print options are not
|
// top border of the poster box; the footer print options did not sit on the bottom one. The
|
||||||
// inline on the border or outline of the box... we want the footer to imitate the header, by
|
// footer now imitates the header, on the edge like a piece of tape.
|
||||||
// being inline like a piece of tape".
|
|
||||||
//
|
//
|
||||||
// top:100% + translate(-50%,-50%) centres it on that edge at ANY height, which matters because
|
// top:100% + translate(-50%,-50%) centres it on that edge at ANY height, which matters because
|
||||||
// the row's height is NOT fixed: it holds two to four controls and grows with translated
|
// the row's height is NOT fixed: it holds two to four controls and grows with translated
|
||||||
|
|
@ -887,8 +882,8 @@ useShadowStyles(
|
||||||
'.pbtn-deck::before{content:"\\25B8";display:inline-block;margin-right:3px;font-size:8px;' +
|
'.pbtn-deck::before{content:"\\25B8";display:inline-block;margin-right:3px;font-size:8px;' +
|
||||||
'transition:transform .15s}' +
|
'transition:transform .15s}' +
|
||||||
'.pbtn-deck[aria-expanded="true"]::before{transform:rotate(90deg)}' +
|
'.pbtn-deck[aria-expanded="true"]::before{transform:rotate(90deg)}' +
|
||||||
// Size choices. These are BUTTONS, not <option>s — the Driver ruled out a native
|
// Size choices. These are BUTTONS, not <option>s — a native dropdown here would
|
||||||
// dropdown here because it collides with the other dropdowns on the host page.
|
// collide with the other dropdowns on the host page.
|
||||||
// Each one both picks the size and prints, so it is styled as an action.
|
// Each one both picks the size and prints, so it is styled as an action.
|
||||||
// ⚠ NO nowrap HERE, and this is a MEASURED correction rather than a preference. With
|
// ⚠ NO nowrap HERE, and this is a MEASURED correction rather than a preference. With
|
||||||
// white-space:nowrap the two landscape labels rendered 161px inside a 169px row and CLIPPED
|
// white-space:nowrap the two landscape labels rendered 161px inside a 169px row and CLIPPED
|
||||||
|
|
@ -905,7 +900,7 @@ useShadowStyles(
|
||||||
// wall: .pbtn 76x20, .pstock 188x20, .tape-h 289x22 -- 135 controls under the minimum on one
|
// wall: .pbtn 76x20, .pstock 188x20, .tape-h 289x22 -- 135 controls under the minimum on one
|
||||||
// phone screen, and the same components fail on three other maps because they live in library/.
|
// phone screen, and the same components fail on three other maps because they live in library/.
|
||||||
// SCOPED TO (pointer:coarse) ON PURPOSE: 2.5.8 is a TOUCH ergonomics rule, and these three
|
// SCOPED TO (pointer:coarse) ON PURPOSE: 2.5.8 is a TOUCH ergonomics rule, and these three
|
||||||
// controls sit in a row whose height the Driver and I tuned by eye this session -- the tape must
|
// controls sit in a row whose height was tuned by eye -- the tape must
|
||||||
// straddle the border without reaching the poster's blue outline, with ~9px of artwork clearance
|
// straddle the border without reaching the poster's blue outline, with ~9px of artwork clearance
|
||||||
// left. Growing them for the mouse would spend 2px of that clearance to solve a problem a mouse
|
// left. Growing them for the mouse would spend 2px of that clearance to solve a problem a mouse
|
||||||
// does not have, on a wall that is another owner's map. A finger gets the bigger target; the
|
// does not have, on a wall that is another owner's map. A finger gets the bigger target; the
|
||||||
|
|
@ -966,9 +961,8 @@ useShadowStyles(
|
||||||
'box-shadow:0 6px 10px -8px rgba(0,0,0,.35);border-radius:4px 4px 0 0}' +
|
'box-shadow:0 6px 10px -8px rgba(0,0,0,.35);border-radius:4px 4px 0 0}' +
|
||||||
'.is-dark .sec-head{border-bottom-color:rgba(255,255,255,.12);background:#0a1410;' +
|
'.is-dark .sec-head{border-bottom-color:rgba(255,255,255,.12);background:#0a1410;' +
|
||||||
'box-shadow:0 6px 10px -8px rgba(0,0,0,.7)}' +
|
'box-shadow:0 6px 10px -8px rgba(0,0,0,.7)}' +
|
||||||
// ★ THE TRANSPARENT STRIP ABOVE A PINNED HEADER (Driver 2026-09-25): "there is a transparent
|
// ★ THE TRANSPARENT STRIP ABOVE A PINNED HEADER: while scrolling, the posters showed through a
|
||||||
// section above the header, so when you're scrolling the posters are visible above the header
|
// transparent band between the pinned header and the top of the page.
|
||||||
// ... in the white space between the header and the top of the page."
|
|
||||||
//
|
//
|
||||||
// CAUSE, MEASURED at 1440px: `position:sticky; top:0` pins to the scroll container's PADDING
|
// CAUSE, MEASURED at 1440px: `position:sticky; top:0` pins to the scroll container's PADDING
|
||||||
// BOX, not its border box. .wall has padding-top:22px, so a pinned header comes to rest 22px
|
// BOX, not its border box. .wall has padding-top:22px, so a pinned header comes to rest 22px
|
||||||
|
|
@ -1011,11 +1005,10 @@ useShadowStyles(
|
||||||
// Poster aspect is 3ft x 3ft SQUARE — the real print ratio, so the wall previews
|
// Poster aspect is 3ft x 3ft SQUARE — the real print ratio, so the wall previews
|
||||||
// the actual artefact rather than an arbitrary box.
|
// the actual artefact rather than an arbitrary box.
|
||||||
// ★ THE BANNER IS NOT THE MAP — and until now it effectively was.
|
// ★ THE BANNER IS NOT THE MAP — and until now it effectively was.
|
||||||
// Driver 2026-09-25, the sticker/olive-oil-bottle brief: this box is a BANNER that DISPLAYS
|
// This box is a BANNER that DISPLAYS the map, the way a label displays a design. A banner taped
|
||||||
// the map, the way a label displays a design. A banner taped to a wall gets a sticker at the
|
// to a wall gets a sticker at the top and one at the bottom, each straddling its edge: a slight
|
||||||
// top and one at the bottom, each straddling its edge — "slight overlap over the banner, but
|
// overlap over the banner, never over the banner's content, because tape over the map would
|
||||||
// not over the content of the banner... if I put a piece of tape over the text it would be
|
// spoil it.
|
||||||
// offensive to the map."
|
|
||||||
//
|
//
|
||||||
// MEASURED before this change, and it is the whole problem: banner 240-426, map 241-425.
|
// MEASURED before this change, and it is the whole problem: banner 240-426, map 241-425.
|
||||||
// whiteSpaceTopOfBanner 1px, whiteSpaceBottomOfBanner 1px. The map filled the banner edge to
|
// whiteSpaceTopOfBanner 1px, whiteSpaceBottomOfBanner 1px. The map filled the banner edge to
|
||||||
|
|
@ -1177,13 +1170,13 @@ useShadowStyles(
|
||||||
not arrive after both bases were tried, which says nothing about whether a
|
not arrive after both bases were tried, which says nothing about whether a
|
||||||
poster exists. A record with genuinely no poster carries its own
|
poster exists. A record with genuinely no poster carries its own
|
||||||
unsupportedReason and renders the branch above. Saying "not generated" of a
|
unsupportedReason and renders the branch above. Saying "not generated" of a
|
||||||
poster that is sitting on disk is how the Driver came to report this.
|
poster that is sitting on disk is how this was found.
|
||||||
i18n DEBT: this literal predates t() here and is still untranslated, like
|
i18n DEBT: this literal predates t() here and is still untranslated, like
|
||||||
the two beside it. -->
|
the two beside it. -->
|
||||||
<div class="ph-note">{{ T('poster.preview_unavailable', 'preview unavailable') }}</div>
|
<div class="ph-note">{{ T('poster.preview_unavailable', 'preview unavailable') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ★ THE PRINT ROW LIVES INSIDE .main, NOT BESIDE IT (Driver 2026-09-25).
|
<!-- ★ THE PRINT ROW LIVES INSIDE .main, NOT BESIDE IT.
|
||||||
It is `position:absolute; top:100%`, which resolves against the nearest
|
It is `position:absolute; top:100%`, which resolves against the nearest
|
||||||
positioned ANCESTOR. As a SIBLING of .main that ancestor was .card, whose
|
positioned ANCESTOR. As a SIBLING of .main that ancestor was .card, whose
|
||||||
bottom sits 26px past the frame — measured: frame bottom 426, row top 452.
|
bottom sits 26px past the frame — measured: frame bottom 426, row top 452.
|
||||||
|
|
@ -1192,7 +1185,7 @@ useShadowStyles(
|
||||||
starts exactly on the border it is taping over. -->
|
starts exactly on the border it is taping over. -->
|
||||||
<fieldset class="tape pfs">
|
<fieldset class="tape pfs">
|
||||||
<legend>{{ t('buttons.printOptions') }}</legend>
|
<legend>{{ t('buttons.printOptions') }}</legend>
|
||||||
<!-- ★ PRAYER LIST IS NOW A DISCLOSURE TOO (Driver 2026-09-28), using the SAME
|
<!-- ★ PRAYER LIST IS NOW A DISCLOSURE TOO, using the SAME
|
||||||
mechanism as Card Deck below — same toggleDeck, same one-open-at-a-time key,
|
mechanism as Card Deck below — same toggleDeck, same one-open-at-a-time key,
|
||||||
same revealed .tape of plain buttons. NOT a <select> and NOT a new component,
|
same revealed .tape of plain buttons. NOT a <select> and NOT a new component,
|
||||||
for the reason already given at the Card Deck comment: a native dropdown
|
for the reason already given at the Card Deck comment: a native dropdown
|
||||||
|
|
@ -1204,11 +1197,9 @@ useShadowStyles(
|
||||||
@click.stop="toggleDeck(item.id, $event, 'packet')">{{ t('buttons.prayerList') }}</button>
|
@click.stop="toggleDeck(item.id, $event, 'packet')">{{ t('buttons.prayerList') }}</button>
|
||||||
<button type="button" class="pbtn" @click="exportPoster(item)">{{ t('buttons.posterMap') }}</button>
|
<button type="button" class="pbtn" @click="exportPoster(item)">{{ t('buttons.posterMap') }}</button>
|
||||||
<!-- ★ CARD DECK IS NOT A PEER OPTION, AND ITS SIZE PICKER IS HIDDEN UNTIL ASKED
|
<!-- ★ CARD DECK IS NOT A PEER OPTION, AND ITS SIZE PICKER IS HIDDEN UNTIL ASKED
|
||||||
FOR (Driver 2026-09-25): "card should not be second option, and I don't want
|
FOR: Cards is not the second option, and a visible dropdown for it would make it
|
||||||
to add the dropdown for the card because it makes it hard for others to have
|
hard for the host page's own dropdowns to coexist; the size choice appears only
|
||||||
dropdowns too... only let you click on the dropdown when you click on print
|
once Print cards is clicked.
|
||||||
the cards, which will show you a hidden dropdown to let you choose the print
|
|
||||||
size option you want."
|
|
||||||
So the deck is a DISCLOSURE, not a sibling button: clicking it reveals this
|
So the deck is a DISCLOSURE, not a sibling button: clicking it reveals this
|
||||||
card's own size row, and the size buttons there are what actually print.
|
card's own size row, and the size buttons there are what actually print.
|
||||||
A 162px <select> was the single widest control in a 172px row — removing it
|
A 162px <select> was the single widest control in a 172px row — removing it
|
||||||
|
|
@ -1230,7 +1221,7 @@ useShadowStyles(
|
||||||
|
|
||||||
<!-- The revealed size row. Rendered as SIBLINGS in a second tape rather than a
|
<!-- The revealed size row. Rendered as SIBLINGS in a second tape rather than a
|
||||||
<select> so there is no native dropdown to collide with other dropdowns —
|
<select> so there is no native dropdown to collide with other dropdowns —
|
||||||
which is the constraint the Driver gave, not a styling preference. -->
|
which is the constraint, not a styling preference. -->
|
||||||
<div v-if="canPrintRows && stockOptions.length && deckOpenFor === deckKey(item.id, 'deck')"
|
<div v-if="canPrintRows && stockOptions.length && deckOpenFor === deckKey(item.id, 'deck')"
|
||||||
class="tape pstocks" role="group" :aria-label="t('buttons.cardStockAria')">
|
class="tape pstocks" role="group" :aria-label="t('buttons.cardStockAria')">
|
||||||
<button v-for="s in stockOptions" :key="s.id" type="button" class="pstock"
|
<button v-for="s in stockOptions" :key="s.id" type="button" class="pstock"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!--
|
<!--
|
||||||
GoalCard.vue — a headline figure with a goal line pinned to the bottom of the card.
|
GoalCard.vue — a headline figure with a goal line pinned to the bottom of the card.
|
||||||
|
|
||||||
The Driver named these directly: "the banners are the cards that tell you the prayer progress".
|
These are the banners: the cards that tell you the prayer progress.
|
||||||
Their RegionDetail.vue renders three of them in a row -- Prayer Coverage, Adoption Progress,
|
Their RegionDetail.vue renders three of them in a row -- Prayer Coverage, Adoption Progress,
|
||||||
Engagement Progress -- identical in structure and different only in their numbers, their bar, and
|
Engagement Progress -- identical in structure and different only in their numbers, their bar, and
|
||||||
one dark variant. That is one component with props, which is why it is here and not three copies.
|
one dark variant. That is one component with props, which is why it is here and not three copies.
|
||||||
|
|
@ -67,8 +67,8 @@ useShadowStyles(
|
||||||
// `.region-goal { margin-top: auto }` then OVERRIDES the same property on the last child,
|
// `.region-goal { margin-top: auto }` then OVERRIDES the same property on the last child,
|
||||||
// so the goal line is pushed to the bottom and the 23.526px stack gap above it is GONE.
|
// so the goal line is pushed to the bottom and the 23.526px stack gap above it is GONE.
|
||||||
// This was ported as `gap`, which margin cannot override: gap AND the auto both applied,
|
// This was ported as `gap`, which margin cannot override: gap AND the auto both applied,
|
||||||
// so every goal card carried ~23.5px of space their cards do not have. The DRIVER saw it
|
// so every goal card carried ~23.5px of space their cards do not have. It showed as uneven
|
||||||
// ('spcing is nto ths ame', naming these three cards). Ported as their mechanism.
|
// spacing on exactly these three cards. Ported as their mechanism.
|
||||||
'.rd-goal-card{ display:flex; flex-direction:column; text-align:center; }' +
|
'.rd-goal-card{ display:flex; flex-direction:column; text-align:center; }' +
|
||||||
'.rd-goal-card > * + *{ margin-top:var(--spacing-lg); }' +
|
'.rd-goal-card > * + *{ margin-top:var(--spacing-lg); }' +
|
||||||
|
|
||||||
|
|
@ -109,7 +109,7 @@ useShadowStyles(
|
||||||
// 59.60px that was breaking. The clamp helps the very problem the flat value was chosen for, and
|
// 59.60px that was breaking. The clamp helps the very problem the flat value was chosen for, and
|
||||||
// the nowrap stays as a belt.
|
// the nowrap stays as a belt.
|
||||||
// cqw not vw, consistent with every other clamp in this port (the container-vs-viewport
|
// cqw not vw, consistent with every other clamp in this port (the container-vs-viewport
|
||||||
// substitution the Driver is still ruling on); at 768 that lands 42.4 against their 46.08.
|
// substitution, provisional, pending a product decision); at 768 that lands 42.4 against their 46.08.
|
||||||
'.rd-big{ margin:0; font-size:clamp(var(--font-size-2xl), 6cqw, var(--font-size-4xl));' +
|
'.rd-big{ margin:0; font-size:clamp(var(--font-size-2xl), 6cqw, var(--font-size-4xl));' +
|
||||||
' font-weight:500; }' +
|
' font-weight:500; }' +
|
||||||
'.rd-goal-card .rd-big{ white-space:nowrap; }' +
|
'.rd-goal-card .rd-big{ white-space:nowrap; }' +
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,8 @@
|
||||||
* RegionSectionCard.vue — one region as a card: a heading, a summary line, a grid of its countries,
|
* RegionSectionCard.vue — one region as a card: a heading, a summary line, a grid of its countries,
|
||||||
* and a footer link. The DX-life /regions card, made reusable.
|
* and a footer link. The DX-life /regions card, made reusable.
|
||||||
*
|
*
|
||||||
* WHY THIS IS IN library/. The Driver, verbatim: "let's say we want to reuse that on future plugins.
|
* WHY THIS IS IN library/. So future plugins can reuse it: a parameterized component where a caller
|
||||||
* We make it a reusable component, parameterized — you can set the region titles wherever you want
|
* sets the region titles and fills in the countries. The design is the point as much
|
||||||
* them to be, fill in the countries to whatever you want them to be." The design is the point as much
|
|
||||||
* as the markup: the 156 lines of CSS below are lifted UNCHANGED from the regions plugin, so a caller
|
* as the markup: the 156 lines of CSS below are lifted UNCHANGED from the regions plugin, so a caller
|
||||||
* gets the look, not a lookalike.
|
* gets the look, not a lookalike.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,8 @@ const menuStyle = ref({})
|
||||||
* tab: every trigger read dLeft=256 dTop=111, with the ancestor walk naming the culprit
|
* tab: every trigger read dLeft=256 dTop=111, with the ancestor walk naming the culprit
|
||||||
* `div.vtl-content [contain:layout paint]`. The same dropdowns in the STANDALONE dashboard read
|
* `div.vtl-content [contain:layout paint]`. The same dropdowns in the STANDALONE dashboard read
|
||||||
* dLeft=0 dTop=4. Same component, same CSS, two hosts, opposite results — which is why a probe
|
* dLeft=0 dTop=4. Same component, same CSS, two hosts, opposite results — which is why a probe
|
||||||
* that only ever measured the standalone composite passed six times while the Driver, who uses
|
* that only ever measured the standalone composite passed six times while the SPA kept showing the
|
||||||
* the SPA, kept reporting the menu "in a random spot further down to the right". It was never
|
* menu in what looked like a random spot, further down and to the right. It was never
|
||||||
* random: it was the rail width plus the header height, exactly.
|
* random: it was the rail width plus the header height, exactly.
|
||||||
*
|
*
|
||||||
* ★ WHY NOT JUST DROP `contain` FROM THE LAYOUT. It is load-bearing there for a different reason
|
* ★ WHY NOT JUST DROP `contain` FROM THE LAYOUT. It is load-bearing there for a different reason
|
||||||
|
|
@ -165,8 +165,8 @@ function positionMenu() {
|
||||||
// unreachable menu is worse than a detached one. That last case cannot mislead: the
|
// unreachable menu is worse than a detached one. That last case cannot mislead: the
|
||||||
// trigger is not on screen to look detached FROM.
|
// trigger is not on screen to look detached FROM.
|
||||||
// ── ★ THE MENU IS NOT THE TRIGGER'S WIDTH (card BUILD-SLIM-FILTERS-NARROW-TRIGGER-WIDE-MENU) ──
|
// ── ★ THE MENU IS NOT THE TRIGGER'S WIDTH (card BUILD-SLIM-FILTERS-NARROW-TRIGGER-WIDE-MENU) ──
|
||||||
// DRIVER: "Make the filters very NARROW, but then the dropdown menu that opens up can be as wide
|
// The filters are very NARROW, and the dropdown menu that opens can be as wide as it needs to
|
||||||
// as you want it to be… so you can fit MORE OF THEM ON ONE ROW WITHOUT WORD WRAPPING."
|
// be, so MORE OF THEM FIT ON ONE ROW WITHOUT WORD WRAPPING.
|
||||||
//
|
//
|
||||||
// THE COUPLING THAT MADE THAT IMPOSSIBLE was this one line: the menu was `min(r.width, …)` — the
|
// THE COUPLING THAT MADE THAT IMPOSSIBLE was this one line: the menu was `min(r.width, …)` — the
|
||||||
// TRIGGER's measured width. The two boxes have OPPOSITE constraints and were sharing one number:
|
// TRIGGER's measured width. The two boxes have OPPOSITE constraints and were sharing one number:
|
||||||
|
|
@ -371,8 +371,8 @@ onBeforeUnmount(() => {
|
||||||
/* ── ★ THE TRIGGER IS A FIXED BOX — IT MUST NOT GROW ON SELECTION ──
|
/* ── ★ THE TRIGGER IS A FIXED BOX — IT MUST NOT GROW ON SELECTION ──
|
||||||
(card BUILD-FILTERS-FIXED-WIDTH-ELLIPSIS-NOT-GROWING)
|
(card BUILD-FILTERS-FIXED-WIDTH-ELLIPSIS-NOT-GROWING)
|
||||||
|
|
||||||
DRIVER: "when you select something from the drop down it should not make the filter wider.
|
Selecting something from the dropdown must not make the filter wider: the filter stays the
|
||||||
The filter should stay the same width. They should just use dot dot dot instead."
|
same width and truncates with an ellipsis instead.
|
||||||
|
|
||||||
MEASURED BEFORE, in BOTH hosts (standalone leaf + cap-5 studio shell), 1600x900:
|
MEASURED BEFORE, in BOTH hosts (standalone leaf + cap-5 studio shell), 1600x900:
|
||||||
empty : [160, 160, 160, 160, 160, 160] total 960
|
empty : [160, 160, 160, 160, 160, 160] total 960
|
||||||
|
|
@ -388,7 +388,7 @@ onBeforeUnmount(() => {
|
||||||
tighter range — it is to stop negotiating: ONE fixed width, and the label ellipsises inside it.
|
tighter range — it is to stop negotiating: ONE fixed width, and the label ellipsises inside it.
|
||||||
|
|
||||||
WHY THIS IS NOT COSMETIC. The selections row sits UNDERNEATH this one. A trigger that grows
|
WHY THIS IS NOT COSMETIC. The selections row sits UNDERNEATH this one. A trigger that grows
|
||||||
reflows the row below it, so the centred stack the Driver asked for could not hold still even
|
reflows the row below it, so the centred stack could not hold still even
|
||||||
once it was centred. Fixing the width here is what makes the alignment above it STRUCTURAL.
|
once it was centred. Fixing the width here is what makes the alignment above it STRUCTURAL.
|
||||||
|
|
||||||
--dx-filter-w is a TOKEN, not a literal, for the same reason every colour here is: this
|
--dx-filter-w is a TOKEN, not a literal, for the same reason every colour here is: this
|
||||||
|
|
@ -398,14 +398,14 @@ onBeforeUnmount(() => {
|
||||||
second word".
|
second word".
|
||||||
|
|
||||||
── ★ 168px -> 128px: THE TRIGGER AND THE MENU ARE SIZED SEPARATELY NOW ──
|
── ★ 168px -> 128px: THE TRIGGER AND THE MENU ARE SIZED SEPARATELY NOW ──
|
||||||
(card BUILD-SLIM-FILTERS-NARROW-TRIGGER-WIDE-MENU) DRIVER, four times: "the filters are way too
|
(card BUILD-SLIM-FILTERS-NARROW-TRIGGER-WIDE-MENU) Asked four times: the filters were far too
|
||||||
wide. Make the filters very NARROW… so you can fit MORE OF THEM ON ONE ROW WITHOUT WORD WRAPPING.
|
wide. Very NARROW filters fit MORE OF THEM ON ONE ROW WITHOUT WORD WRAPPING, and that in turn
|
||||||
And that way we can decrease the thickness of the headers."
|
lets the headers get thinner.
|
||||||
|
|
||||||
The trigger no longer has to carry the SELECTED VALUE legibly, because positionMenu() now floors
|
The trigger no longer has to carry the SELECTED VALUE legibly, because positionMenu() now floors
|
||||||
the open menu at 240px independent of this width (see its MENU_MIN note). That is what unlocks
|
the open menu at 240px independent of this width (see its MENU_MIN note). That is what unlocks
|
||||||
the narrowing: the full value is always readable in the menu, so the closed trigger only has to
|
the narrowing: the full value is always readable in the menu, so the closed trigger only has to
|
||||||
carry the AXIS NAME plus an ellipsis hint — which is all the Driver ever reads it for.
|
carry the AXIS NAME plus an ellipsis hint — which is all a reader uses it for.
|
||||||
|
|
||||||
128px IS MEASURED, NOT PICKED — and the first number tried (116px) was measured WRONG, which is
|
128px IS MEASURED, NOT PICKED — and the first number tried (116px) was measured WRONG, which is
|
||||||
why the measurement is recorded here rather than the conclusion alone. The chrome budget is:
|
why the measurement is recorded here rather than the conclusion alone. The chrome budget is:
|
||||||
|
|
@ -427,8 +427,8 @@ onBeforeUnmount(() => {
|
||||||
allowed to move (below). If an axis longer than "Engagement" is ever added, re-measure rather
|
allowed to move (below). If an axis longer than "Engagement" is ever added, re-measure rather
|
||||||
than letting it truncate.
|
than letting it truncate.
|
||||||
|
|
||||||
★ THE FONT-SIZE IS NOT A LEVER. The Driver has rejected shrinking it twice ("super tiny text
|
★ THE FONT-SIZE IS NOT A LEVER: shrinking it has been rejected twice, because text too small to
|
||||||
that cannot be read"). Width, padding and TRIGGER-label truncation are the only tools; 0.875rem
|
read is not an option. Width, padding and TRIGGER-label truncation are the only tools; 0.875rem
|
||||||
on .filter-dropdown__trigger is load-bearing and must not be traded for density. */
|
on .filter-dropdown__trigger is load-bearing and must not be traded for density. */
|
||||||
.filter-dropdown { position: relative; flex: 0 0 auto;
|
.filter-dropdown { position: relative; flex: 0 0 auto;
|
||||||
width: var(--dx-filter-w, 128px); min-width: var(--dx-filter-w, 128px); max-width: var(--dx-filter-w, 128px); }
|
width: var(--dx-filter-w, 128px); min-width: var(--dx-filter-w, 128px); max-width: var(--dx-filter-w, 128px); }
|
||||||
|
|
|
||||||
|
|
@ -190,19 +190,18 @@ function onClearAll() { emit('clear-all') }
|
||||||
<!-- ★ THE SELECTIONS ROW IS PERMANENT — it must never leave the DOM.
|
<!-- ★ THE SELECTIONS ROW IS PERMANENT — it must never leave the DOM.
|
||||||
This was v-if="useFilters && chips.length", so the row DISAPPEARED whenever nothing was
|
This was v-if="useFilters && chips.length", so the row DISAPPEARED whenever nothing was
|
||||||
selected. That is the actual bug, and it is layout shift rather than chip placement:
|
selected. That is the actual bug, and it is layout shift rather than chip placement:
|
||||||
"whenever I deselect it, it removes that space from the second header... the decompression
|
deselecting removed the row's height, so the ENTIRE LOWER APP SHIFTED UPWARD, which is very
|
||||||
of the height causes the ENTIRE LOWER APP TO SHIFT UPWARD, which makes it really hard to
|
hard to track with the eyes.
|
||||||
track with your eyes." (Driver, 2026-09-22)
|
|
||||||
A row that appears on select and vanishes on clear moves the map, the table and the grid on
|
A row that appears on select and vanishes on clear moves the map, the table and the grid on
|
||||||
every single click. The row now always renders and only its CONTENTS change, so nothing below
|
every single click. The row now always renders and only its CONTENTS change, so nothing below
|
||||||
it can move.
|
it can move.
|
||||||
The empty state is "World" — the Driver's word, and it reads correctly too: no filter really
|
The empty state is "World", and it reads correctly: no filter really
|
||||||
does mean the whole world is in scope. Clear-all is omitted rather than the row collapsing,
|
does mean the whole world is in scope. Clear-all is omitted rather than the row collapsing,
|
||||||
because there is nothing to clear; the row keeps its height either way. -->
|
because there is nothing to clear; the row keeps its height either way. -->
|
||||||
<!-- ★ CLEAR ALL TRAVELS WITH THE CHIPS — it is the LAST CHILD OF THE SCROLL STRIP
|
<!-- ★ CLEAR ALL TRAVELS WITH THE CHIPS — it is the LAST CHILD OF THE SCROLL STRIP
|
||||||
(card BUILD-CLEAR-ALL-SITS-BESIDE-THE-SELECTIONS-NOT-FAR-RIGHT).
|
(card BUILD-CLEAR-ALL-SITS-BESIDE-THE-SELECTIONS-NOT-FAR-RIGHT).
|
||||||
DRIVER: "the clear all button is to look fancy, fancy Nancy. And it doesn't need to be at
|
The Clear All button should look polished, and it belongs right next to the
|
||||||
the far right. It needs to be right next to the selected stuff."
|
selections, not at the far right.
|
||||||
|
|
||||||
THIS DELIBERATELY REVERSES the previous instruction (kept below for the record, because
|
THIS DELIBERATELY REVERSES the previous instruction (kept below for the record, because
|
||||||
the reasoning was sound and someone will be tempted to re-apply it): Clear All used to be
|
the reasoning was sound and someone will be tempted to re-apply it): Clear All used to be
|
||||||
|
|
@ -285,7 +284,7 @@ function onClearAll() { emit('clear-all') }
|
||||||
which is why FilterDropdown's menu is position:FIXED and escapes the clip chain entirely
|
which is why FilterDropdown's menu is position:FIXED and escapes the clip chain entirely
|
||||||
rather than trying to out-z-index it. Do not "restore" an overflow-y here; it cannot work. */
|
rather than trying to out-z-index it. Do not "restore" an overflow-y here; it cannot work. */
|
||||||
/* ★ CENTRED AS A GROUP, EVENLY SPACED (card BUILD-THE-FOUR-HEADERS-ARE-ONE-CENTRED-STACK).
|
/* ★ CENTRED AS A GROUP, EVENLY SPACED (card BUILD-THE-FOUR-HEADERS-ARE-ONE-CENTRED-STACK).
|
||||||
DRIVER: "evenly spaced out so that everything is centred, not justified left."
|
Evenly spaced, so that everything is centred rather than justified left.
|
||||||
|
|
||||||
MEASURED BEFORE at 1600x900 standalone: the triggers started 14px from the left edge and ended
|
MEASURED BEFORE at 1600x900 standalone: the triggers started 14px from the left edge and ended
|
||||||
242px from the right — a quarter-screen of dead space on one side only, which is precisely what
|
242px from the right — a quarter-screen of dead space on one side only, which is precisely what
|
||||||
|
|
@ -302,15 +301,15 @@ function onClearAll() { emit('clear-all') }
|
||||||
The unprefixed fallback is listed first for engines without `safe` — they get plain centring,
|
The unprefixed fallback is listed first for engines without `safe` — they get plain centring,
|
||||||
which is the pre-existing behaviour for the fitting case and no worse for the other. */
|
which is the pre-existing behaviour for the fitting case and no worse for the other. */
|
||||||
/* ★ THE SLACK GOES BETWEEN THE TRIGGERS, NOT INTO TWO EDGE VOIDS
|
/* ★ THE SLACK GOES BETWEEN THE TRIGGERS, NOT INTO TWO EDGE VOIDS
|
||||||
(card BUILD-CENTRE-THE-SEARCH-BAR-AND-SPREAD-THE-FILTER-TOGGLES). DRIVER: "the dropdown
|
(card BUILD-CENTRE-THE-SEARCH-BAR-AND-SPREAD-THE-FILTER-TOGGLES). The filter dropdown
|
||||||
toggles for the filters need to be centred also, so that they FILL UP THE FULL WIDTH, equally
|
toggles are centred too, so that they FILL THE FULL WIDTH of the second header, equally
|
||||||
spaced to fill up the second header."
|
spaced.
|
||||||
|
|
||||||
MEASURED at 1600x900 standalone BEFORE this change: the row is 1572px wide and its content is
|
MEASURED at 1600x900 standalone BEFORE this change: the row is 1572px wide and its content is
|
||||||
6 triggers x 168px + 5 gaps x 10px = 1058px — so scrollWidth (1572) == clientWidth (1572),
|
6 triggers x 168px + 5 gaps x 10px = 1058px — so scrollWidth (1572) == clientWidth (1572),
|
||||||
`overflowing: false`, with 514px of slack. Centring alone therefore parks ~257px of dead space
|
`overflowing: false`, with 514px of slack. Centring alone therefore parks ~257px of dead space
|
||||||
at EACH edge, which is what "not filling the width" looks like in numbers. The row is NOT
|
at EACH edge, which is what "not filling the width" looks like in numbers. The row is NOT
|
||||||
overflowing at desktop width, so the horizontal scrollbar the Driver complained about is a
|
overflowing at desktop width, so the reported horizontal scrollbar is a
|
||||||
narrow-viewport artifact, not the desktop case.
|
narrow-viewport artifact, not the desktop case.
|
||||||
|
|
||||||
`space-between` would push the first and last triggers hard against the edges, breaking the
|
`space-between` would push the first and last triggers hard against the edges, breaking the
|
||||||
|
|
@ -328,14 +327,14 @@ function onClearAll() { emit('clear-all') }
|
||||||
The studio host was measured overflowing once already (right gutter -40), so this is a live
|
The studio host was measured overflowing once already (right gutter -40), so this is a live
|
||||||
case, not a hypothetical — do not simplify this to a single unguarded value. */
|
case, not a hypothetical — do not simplify this to a single unguarded value. */
|
||||||
/* ★ WRAP, DO NOT SCROLL (2026-09-23). This row was flex-wrap:nowrap + overflow-x:auto — a
|
/* ★ WRAP, DO NOT SCROLL (2026-09-23). This row was flex-wrap:nowrap + overflow-x:auto — a
|
||||||
horizontal SCROLLER. The Driver's constraint is explicit: "I shouldn't see any horizontal scroll
|
horizontal SCROLLER. The constraint is explicit: no horizontal scrollbar in the filter section;
|
||||||
bar in the filter section. You need to resize everything, but everything is visible."
|
everything resizes and everything stays visible.
|
||||||
MEASURED with the eighth dropdown (engagement/Status) added: the dropdowns are a fixed 168px on a
|
MEASURED with the eighth dropdown (engagement/Status) added: the dropdowns are a fixed 168px on a
|
||||||
200px pitch, so eight need ~1428px. At a 1440 viewport that just fits (maxRight 1428); at 1280 it
|
200px pitch, so eight need ~1428px. At a 1440 viewport that just fits (maxRight 1428); at 1280 it
|
||||||
did not, and sfh__filters-row reported scrollWidth 1414 against clientWidth 1252 — a scrollbar,
|
did not, and sfh__filters-row reported scrollWidth 1414 against clientWidth 1252 — a scrollbar,
|
||||||
exactly what he asked not to see.
|
exactly what he asked not to see.
|
||||||
Wrapping is the right lever rather than shrinking the dropdowns: their fixed 168px width is itself
|
Wrapping is the right lever rather than shrinking the dropdowns: their fixed 168px width is itself
|
||||||
a Driver decision ("filters not so wide"), and a flexible width was what made them jump as a
|
a product decision (filters not so wide), and a flexible width was what made them jump as a
|
||||||
selection changed. So the row now flows onto a second line when it must.
|
selection changed. So the row now flows onto a second line when it must.
|
||||||
★ AND START THE WRAPPED LINE ON THE SAME LEFT EDGE (2026-09-23). `space-evenly` was kept here
|
★ AND START THE WRAPPED LINE ON THE SAME LEFT EDGE (2026-09-23). `space-evenly` was kept here
|
||||||
when wrapping landed, on the reasoning that `safe` made it harmless. That reasoning was about a
|
when wrapping landed, on the reasoning that `safe` made it harmless. That reasoning was about a
|
||||||
|
|
@ -384,12 +383,12 @@ function onClearAll() { emit('clear-all') }
|
||||||
set TOGETHER, every time either moves.
|
set TOGETHER, every time either moves.
|
||||||
|
|
||||||
★ 18px, AND THE OLD 24px MEASUREMENT IS NOT EVIDENCE IT WAS RIGHT
|
★ 18px, AND THE OLD 24px MEASUREMENT IS NOT EVIDENCE IT WAS RIGHT
|
||||||
(card BUILD-SELECTED-ROW-MUCH-THINNER-AND-CENTRED). DRIVER, twice in one message: "the selected
|
(card BUILD-SELECTED-ROW-MUCH-THINNER-AND-CENTRED). Asked twice in one message: the selected
|
||||||
row is way too tall, needs to be thinner, a lot thinner, a lot more subtle."
|
row was far too tall, and needed to be a lot thinner and a lot more subtle.
|
||||||
A previous fix asserted rowH=24 identical across empty / one-chip / many-chip states and cited it
|
A previous fix asserted rowH=24 identical across empty / one-chip / many-chip states and cited it
|
||||||
as done. That measurement was TRUE and it proved the row does not RESIZE — it never said a word
|
as done. That measurement was TRUE and it proved the row does not RESIZE — it never said a word
|
||||||
about whether 24 is the right number, which is a different question that only the Driver can
|
about whether 24 is the right number, which is a different question, a product one. The
|
||||||
answer. He has now answered it: still too tall. Do not re-cite a stability measurement as a
|
answer came back: still too tall. Do not re-cite a stability measurement as a
|
||||||
sizing one.
|
sizing one.
|
||||||
He named THREE height sources, so all three come down together — reducing only the row would
|
He named THREE height sources, so all three come down together — reducing only the row would
|
||||||
have been the same partial fix again:
|
have been the same partial fix again:
|
||||||
|
|
@ -398,8 +397,8 @@ function onClearAll() { emit('clear-all') }
|
||||||
3. the CHIP BUTTONS -> .sfh__chip height 22 -> 18, font 0.9375rem -> 0.8125rem
|
3. the CHIP BUTTONS -> .sfh__chip height 22 -> 18, font 0.9375rem -> 0.8125rem
|
||||||
--dx-sel-row-h keeps the floor tokenized so a consumer can trade density back without forking.
|
--dx-sel-row-h keeps the floor tokenized so a consumer can trade density back without forking.
|
||||||
|
|
||||||
★ CENTRED: justify-content:center, because the Driver puts the selections directly above the map
|
★ CENTRED: justify-content:center, because the selections sit directly above the map, centred
|
||||||
— "that needs to be centred... right above the globe, centred, is going to be what is selected."
|
right above the globe.
|
||||||
★ THE ROW ITSELF STILL DOES NOT SCROLL. overflow-x lives on .sfh__chip-strip. That split is now
|
★ THE ROW ITSELF STILL DOES NOT SCROLL. overflow-x lives on .sfh__chip-strip. That split is now
|
||||||
doing a different job than when it was introduced (it was there to pin Clear All OUTSIDE the
|
doing a different job than when it was introduced (it was there to pin Clear All OUTSIDE the
|
||||||
scroller; Clear All has since moved INSIDE, per its own card) — it is kept because a scroll box
|
scroller; Clear All has since moved INSIDE, per its own card) — it is kept because a scroll box
|
||||||
|
|
@ -418,7 +417,7 @@ function onClearAll() { emit('clear-all') }
|
||||||
would widen the row instead of scrolling inside itself.
|
would widen the row instead of scrolling inside itself.
|
||||||
★ flex is now `0 1 auto`, not `1 1 auto`. Growing was only needed while Clear All sat OUTSIDE
|
★ flex is now `0 1 auto`, not `1 1 auto`. Growing was only needed while Clear All sat OUTSIDE
|
||||||
as a pinned sibling — the strip had to eat all the free space to shove it to the far right.
|
as a pinned sibling — the strip had to eat all the free space to shove it to the far right.
|
||||||
That is the layout the Driver just reversed, so the growth term goes with it: at 0 1 auto the
|
That is the layout that was just reversed, so the growth term goes with it: at 0 1 auto the
|
||||||
strip is only as wide as its chips (shrinking to fit when they overflow), which is what lets
|
strip is only as wide as its chips (shrinking to fit when they overflow), which is what lets
|
||||||
.sfh__chips centre it as a group rather than centring a full-width box that merely CONTAINS
|
.sfh__chips centre it as a group rather than centring a full-width box that merely CONTAINS
|
||||||
left-aligned chips. Keeping flex-grow here would have silently defeated the centring above.
|
left-aligned chips. Keeping flex-grow here would have silently defeated the centring above.
|
||||||
|
|
@ -440,8 +439,8 @@ function onClearAll() { emit('clear-all') }
|
||||||
would have been a fixed-width item that the centring had to work around, pushing the chips
|
would have been a fixed-width item that the centring had to work around, pushing the chips
|
||||||
off-centre by its own width. It scrolls with the chips now, which is the same trade Clear All
|
off-centre by its own width. It scrolls with the chips now, which is the same trade Clear All
|
||||||
makes and for the same reason: the label describes the chips, so it belongs with them.
|
makes and for the same reason: the label describes the chips, so it belongs with them.
|
||||||
0.6875rem + subtler colour: this is a LABEL, not content — the Driver asked for the row to read
|
0.6875rem + subtler colour: this is a LABEL, not content — the row is to read a lot more
|
||||||
"a lot more subtle", and shrinking the all-caps prefix is the cheapest way to drop the row's
|
subtly, and shrinking the all-caps prefix is the cheapest way to drop the row's
|
||||||
visual weight without touching the chips' legibility. */
|
visual weight without touching the chips' legibility. */
|
||||||
.sfh__sel-label { flex: 0 0 auto; font-size: 0.6875rem; font-weight: 700; letter-spacing: .5px;
|
.sfh__sel-label { flex: 0 0 auto; font-size: 0.6875rem; font-weight: 700; letter-spacing: .5px;
|
||||||
text-transform: uppercase; color: var(--mut, #6d766e); opacity: .75; }
|
text-transform: uppercase; color: var(--mut, #6d766e); opacity: .75; }
|
||||||
|
|
@ -452,8 +451,8 @@ function onClearAll() { emit('clear-all') }
|
||||||
/* ★ SOURCE 3 OF THE THREE HEIGHT SOURCES — "the size of the buttons that show up whenever you
|
/* ★ SOURCE 3 OF THE THREE HEIGHT SOURCES — "the size of the buttons that show up whenever you
|
||||||
select something." 22px -> 18px, and the type comes DOWN with it (0.9375rem -> 0.8125rem).
|
select something." 22px -> 18px, and the type comes DOWN with it (0.9375rem -> 0.8125rem).
|
||||||
NOTE THIS REVERSES AN EARLIER, CORRECTLY-FOLLOWED INSTRUCTION. A previous round was told
|
NOTE THIS REVERSES AN EARLIER, CORRECTLY-FOLLOWED INSTRUCTION. A previous round was told
|
||||||
"increase the font slightly of the selected row", and did: 0.85rem -> 0.9375rem. The Driver is
|
"increase the font slightly of the selected row", and did: 0.85rem -> 0.9375rem. The whole row
|
||||||
now asking for the whole row to be "a lot more subtle", and at 18px a 0.9375rem glyph does not
|
is now to be a lot more subtle, and at 18px a 0.9375rem glyph does not
|
||||||
fit its box. The newer instruction wins; this is not a regression of the older one.
|
fit its box. The newer instruction wins; this is not a regression of the older one.
|
||||||
white-space:nowrap keeps a two-word label on one line — a wrapped chip would grow the row and
|
white-space:nowrap keeps a two-word label on one line — a wrapped chip would grow the row and
|
||||||
re-break the fixed-height invariant.
|
re-break the fixed-height invariant.
|
||||||
|
|
@ -477,7 +476,7 @@ function onClearAll() { emit('clear-all') }
|
||||||
the user the whole view.
|
the user the whole view.
|
||||||
THE ROW HEIGHT IS NOT NEGOTIABLE and must not be touched here: the three comment blocks above
|
THE ROW HEIGHT IS NOT NEGOTIABLE and must not be touched here: the three comment blocks above
|
||||||
record a measured layout-shift bug (18px vs a 24.25px chipped row walking the map 180 -> 182.25px)
|
record a measured layout-shift bug (18px vs a 24.25px chipped row walking the map 180 -> 182.25px)
|
||||||
and the Driver's "a lot more subtle" instruction that set 16px. Growing the button would reopen
|
and the "a lot more subtle" instruction that set 16px. Growing the button would reopen
|
||||||
both.
|
both.
|
||||||
So the HIT AREA grows and the BOX does not. An absolutely-positioned ::after centred on the
|
So the HIT AREA grows and the BOX does not. An absolutely-positioned ::after centred on the
|
||||||
button claims 24x24 of pointer/touch surface while contributing nothing to the flex row's
|
button claims 24x24 of pointer/touch surface while contributing nothing to the flex row's
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ function exactIndices(haystack, needle) {
|
||||||
// (threshold 0.4, ignoreLocation) tolerates TYPOS -- an extra trailing letter ("Bhutann"),
|
// (threshold 0.4, ignoreLocation) tolerates TYPOS -- an extra trailing letter ("Bhutann"),
|
||||||
// a substituted one ("Indoa") -- because bitap scores k errors against the pattern length.
|
// a substituted one ("Indoa") -- because bitap scores k errors against the pattern length.
|
||||||
// A subsequence can only ever DROP haystack chars; an EXTRA needle char always fails it.
|
// A subsequence can only ever DROP haystack chars; an EXTRA needle char always fails it.
|
||||||
// Measured on the live dashboard: 'Bhutan' -> 13 rows, 'Bhutann' -> 0. The Driver's test.
|
// Measured on the live dashboard: 'Bhutan' -> 13 rows, 'Bhutann' -> 0, the reported test.
|
||||||
//
|
//
|
||||||
// So this is the standard approximate-substring DP (Sellers/Ukkonen "k-errors" matching):
|
// So this is the standard approximate-substring DP (Sellers/Ukkonen "k-errors" matching):
|
||||||
// best edit distance of the needle against ANY window of the haystack (free start, free end),
|
// best edit distance of the needle against ANY window of the haystack (free start, free end),
|
||||||
|
|
@ -63,7 +63,7 @@ function approxIndices(haystack, needle, threshold) {
|
||||||
// ★ ABSOLUTE CAP AT 2 EDITS (the classic did-you-mean bound). threshold*len alone grows with
|
// ★ ABSOLUTE CAP AT 2 EDITS (the classic did-you-mean bound). threshold*len alone grows with
|
||||||
// the term: at 8+ chars it allowed 3 errors, and 3-edit windows are how 'Canadian' -- a country
|
// the term: at 8+ chars it allowed 3 errors, and 3-edit windows are how 'Canadian' -- a country
|
||||||
// with ZERO rows in this dataset -- raked 47 rows of unrelated facets. Every real typo the
|
// with ZERO rows in this dataset -- raked 47 rows of unrelated facets. Every real typo the
|
||||||
// Driver reported (Bhutann, Banladesh, Austrian->Austria, Canadian->Canada) is 1-2 edits.
|
// reported (Bhutann, Banladesh, Austrian->Austria, Canadian->Canada) is 1-2 edits.
|
||||||
const maxErr = Math.min(2, Math.floor(threshold * m))
|
const maxErr = Math.min(2, Math.floor(threshold * m))
|
||||||
if (maxErr <= 0) return null
|
if (maxErr <= 0) return null
|
||||||
// dp[j] = min errors matching the whole prefix n[0..i] ending at text position j.
|
// dp[j] = min errors matching the whole prefix n[0..i] ending at text position j.
|
||||||
|
|
@ -88,9 +88,8 @@ function approxIndices(haystack, needle, threshold) {
|
||||||
/**
|
/**
|
||||||
* Low-level predicates shared with the dashboard's search matcher (useDashboardShell.js).
|
* Low-level predicates shared with the dashboard's search matcher (useDashboardShell.js).
|
||||||
*
|
*
|
||||||
* ★ TIERED, NOT A UNION (Driver 2026-09-24: "fuzzy search is too fuzzy -- Bhutan returns more
|
* ★ TIERED, NOT A UNION: search was too fuzzy (Bhutan returned more than the country) and did not
|
||||||
* results than just the country, and there is no interoperability with the other filters like
|
* agree with the other filters, such as choosing it as a Place. The first fuzzy pass returned exact matches PLUS every <=k-error
|
||||||
* selecting it as a place"). The first fuzzy pass returned exact matches PLUS every <=k-error
|
|
||||||
* neighbour, so the EXACT term 'Bhutan' counted 132 rows while the Country filter counted 13 --
|
* neighbour, so the EXACT term 'Bhutan' counted 132 rows while the Country filter counted 13 --
|
||||||
* two controls disagreeing about the same place. The conventional contract is tiered: when a
|
* two controls disagreeing about the same place. The conventional contract is tiered: when a
|
||||||
* term has exact matches, return ONLY those (search now agrees with the facet, 13 = 13); the
|
* term has exact matches, return ONLY those (search now agrees with the facet, 13 = 13); the
|
||||||
|
|
@ -106,9 +105,8 @@ export function exactMatches(text, term) {
|
||||||
return raw.toLowerCase().includes(q.toLowerCase())
|
return raw.toLowerCase().includes(q.toLowerCase())
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* ★ TIER 2 — FACET-VALUE RESOLUTION (Driver 2026-09-24: "if I search the name of the country
|
* ★ TIER 2 — FACET-VALUE RESOLUTION: a country's name with an n added (Austrian, Canadian) selects
|
||||||
* and add n to it, like Austrian or Canadian, I would like for there to be a selection of that
|
* that country in the search results, so the table under the map shows that country's result.
|
||||||
* country in the search results -- the table under the map shows the result for that country").
|
|
||||||
* Given the DISTINCT values of a facet (country labels, region names...), return the values the
|
* Given the DISTINCT values of a facet (country labels, region names...), return the values the
|
||||||
* term fuzzy-resolves to. A term like 'Austrian' then selects rows by country === 'Austria' --
|
* term fuzzy-resolves to. A term like 'Austrian' then selects rows by country === 'Austria' --
|
||||||
* byte-identical to picking the country as a Place filter -- instead of raking every text field
|
* byte-identical to picking the country as a Place filter -- instead of raking every text field
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
*
|
*
|
||||||
* ★ THE ONE print mechanism, graduated VERBATIM from my-upg-100-list/poster-wall.vue
|
* ★ THE ONE print mechanism, graduated VERBATIM from my-upg-100-list/poster-wall.vue
|
||||||
* openPrintWindow() (commit e957cbe "Print the poster in place, on a portrait sheet") on the
|
* openPrintWindow() (commit e957cbe "Print the poster in place, on a portrait sheet") on the
|
||||||
* Driver's ruling that killed the pop-up path: print must happen IN PLACE — no new browser
|
* ruling that killed the pop-up path: print must happen IN PLACE — no new browser
|
||||||
* window, no pop-up blocker, the dialog rises over the page the user is looking at.
|
* window, no pop-up blocker, the dialog rises over the page the user is looking at.
|
||||||
*
|
*
|
||||||
* WHY IT GRADUATES (library high bar: reusable + parameterized, 2+ consumers): the
|
* WHY IT GRADUATES (library high bar: reusable + parameterized, 2+ consumers): the
|
||||||
* prayer-walk-planner's print still used window.open(), and that is not just the rejected UX --
|
* prayer-walk-planner's print still used window.open(), and that is not just the rejected UX --
|
||||||
* it is AGENT-HOSTILE. window.open() without a user gesture is pop-up-blocked, so
|
* it is AGENT-HOSTILE. window.open() without a user gesture is pop-up-blocked, so
|
||||||
* PrayerWalkPlanner.print() called from an agent (the Driver: "the agent can also create
|
* PrayerWalkPlanner.print() called from an agent (an agent can also create polygons and
|
||||||
* polygons and trigger the print function") silently did nothing. An iframe cannot be blocked
|
* trigger the print function) silently did nothing. An iframe cannot be blocked
|
||||||
* and needs no gesture, which is what makes the same call work for a human click AND an agent.
|
* and needs no gesture, which is what makes the same call work for a human click AND an agent.
|
||||||
*
|
*
|
||||||
* Mechanics preserved from the poster-wall original, each for a measured reason:
|
* Mechanics preserved from the poster-wall original, each for a measured reason:
|
||||||
|
|
@ -49,7 +49,7 @@ export function openPrintFrame(bodyHtml, { autoprint = false } = {}) {
|
||||||
/**
|
/**
|
||||||
* Print a rendered poster IMAGE, at its real paper size, through the same in-place frame.
|
* Print a rendered poster IMAGE, at its real paper size, through the same in-place frame.
|
||||||
*
|
*
|
||||||
* ★ THE DRIVER'S RULE (2026-09-28): an "Export poster" button opens the PRINT DIALOG with a preview.
|
* ★ THE RULE (2026-09-28): an "Export poster" button opens the PRINT DIALOG with a preview.
|
||||||
* It never hands over a file. Simple Research was rendering a PNG and calling a download helper, so
|
* It never hands over a file. Simple Research was rendering a PNG and calling a download helper, so
|
||||||
* the reader got doxa-prayer-poster.png in their downloads folder and no preview, no paper size, and
|
* the reader got doxa-prayer-poster.png in their downloads folder and no preview, no paper size, and
|
||||||
* no way to print it without opening it somewhere else first. The dialog IS the preview: the sheet the
|
* no way to print it without opening it somewhere else first. The dialog IS the preview: the sheet the
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
*
|
*
|
||||||
* ★ WHY (card BUG-LAZY-PLUGIN-DEATH-ON-A-SINGLE-FAILED-IMPORT). Vue's defineAsyncComponent
|
* ★ WHY (card BUG-LAZY-PLUGIN-DEATH-ON-A-SINGLE-FAILED-IMPORT). Vue's defineAsyncComponent
|
||||||
* caches a REJECTED loader promise: once a dynamic import() fails, that component is dead for
|
* caches a REJECTED loader promise: once a dynamic import() fails, that component is dead for
|
||||||
* the life of the page — every later render replays the same rejection. The Driver hit exactly
|
* the life of the page — every later render replays the same rejection. It happened live, exactly
|
||||||
* this: one net::ERR_NETWORK_CHANGED event (wifi switch / VPN / sleep-wake, a TRANSPORT blip,
|
* like this: one net::ERR_NETWORK_CHANGED event (wifi switch / VPN / sleep-wake, a TRANSPORT blip,
|
||||||
* not a 404) failed five unrelated module fetches at the same moment, and doxa-simple-map +
|
* not a 404) failed five unrelated module fetches at the same moment, and doxa-simple-map +
|
||||||
* doxa-research-map stayed "not loading" until a full reload — a working product reading as
|
* doxa-research-map stayed "not loading" until a full reload — a working product reading as
|
||||||
* two broken plugins.
|
* two broken plugins.
|
||||||
|
|
|
||||||
|
|
@ -234,8 +234,7 @@ export function useCsvExport({ rowStore, rows, selectionSet, filterSet, FACET_AC
|
||||||
const provenance = `# ${exportLabel} export — scope: ${scopeLabel()}; filters: ${chips}; rows: ${picked.length}`
|
const provenance = `# ${exportLabel} export — scope: ${scopeLabel()}; filters: ${chips}; rows: ${picked.length}`
|
||||||
+ (sources ? `; sources: ${sources}` : '')
|
+ (sources ? `; sources: ${sources}` : '')
|
||||||
// ★ THE PROVENANCE LINE GOES LAST, NOT FIRST — that one position was the whole "glitched CSV".
|
// ★ THE PROVENANCE LINE GOES LAST, NOT FIRST — that one position was the whole "glitched CSV".
|
||||||
// Driver-reported 2026-09-22: "this is me exporting the CSV directly without making any edits...
|
// Reported 2026-09-22: a CSV exported directly, with no edits, looked a little glitched out.
|
||||||
// it does look a little bit glitched out."
|
|
||||||
//
|
//
|
||||||
// MEASURED with a strict RFC-4180 parser against this exact generator. EVERYTHING ELSE IS CORRECT
|
// MEASURED with a strict RFC-4180 parser against this exact generator. EVERYTHING ELSE IS CORRECT
|
||||||
// and must not be "fixed" on suspicion:
|
// and must not be "fixed" on suspicion:
|
||||||
|
|
@ -244,7 +243,7 @@ export function useCsvExport({ rowStore, rows, selectionSet, filterSet, FACET_AC
|
||||||
// rows ... N columns each, ZERO with a wrong column count
|
// rows ... N columns each, ZERO with a wrong column count
|
||||||
// embedded commas, embedded double quotes AND embedded newlines all survive the round trip, so
|
// embedded commas, embedded double quotes AND embedded newlines all survive the round trip, so
|
||||||
// esc() is genuinely RFC-4180 (quote when , " CR LF appear; double the internal quotes). The
|
// esc() is genuinely RFC-4180 (quote when , " CR LF appear; double the internal quotes). The
|
||||||
// file is comma-separated, not tab-separated — the tabs in the Driver's paste were his terminal
|
// file is comma-separated, not tab-separated — the tabs in the pasted report were a terminal's
|
||||||
// rendering, not the bytes.
|
// rendering, not the bytes.
|
||||||
//
|
//
|
||||||
// The defect is purely POSITIONAL: every spreadsheet importer treats the FIRST line as the header.
|
// The defect is purely POSITIONAL: every spreadsheet importer treats the FIRST line as the header.
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,8 @@
|
||||||
* the table's share is derived as shellH - mapH (see the CSS in research-dashboard.vue). Before
|
* the table's share is derived as shellH - mapH (see the CSS in research-dashboard.vue). Before
|
||||||
* this, --rd-map-h alone was BOTH "the map/table split" AND "the element's total height" — one
|
* this, --rd-map-h alone was BOTH "the map/table split" AND "the element's total height" — one
|
||||||
* knob answering two questions, which is why dragging the map/table divider used to resize the
|
* knob answering two questions, which is why dragging the map/table divider used to resize the
|
||||||
* whole element as a side effect. Driver: "you improperly bound that control to the separator
|
* whole element as a side effect: the divider between the map and the table was bound to the
|
||||||
* line between the map and the table... that's not the request I was making to resize the
|
* page's height, which is not what that control is for.
|
||||||
* height of the page."
|
|
||||||
*
|
*
|
||||||
* COMPOSITION with the collapse-table fix (do NOT regress):
|
* COMPOSITION with the collapse-table fix (do NOT regress):
|
||||||
* The shell's DEFAULT rows are `52px auto 42px 1fr var(--table-h,34%)` while the collapsed rule
|
* The shell's DEFAULT rows are `52px auto 42px 1fr var(--table-h,34%)` while the collapsed rule
|
||||||
|
|
@ -106,34 +105,29 @@ const DEFAULT_STORAGE_KEY = 'rd-dashboard-layout-v6'
|
||||||
// shellH default (720) is the SUM the old single-knob mapH(360) used to imply — map 360 + table
|
// shellH default (720) is the SUM the old single-knob mapH(360) used to imply — map 360 + table
|
||||||
// budget 360 (328 grid + 32 bar) — so first paint is pixel-identical to before this axis split;
|
// budget 360 (328 grid + 32 bar) — so first paint is pixel-identical to before this axis split;
|
||||||
// only DRAGGING behaves differently now (see the SHELL vs. TABLE axis note above).
|
// only DRAGGING behaves differently now (see the SHELL vs. TABLE axis note above).
|
||||||
// ★ shellH 720 -> 602 so the table OPENS at seven data rows (Driver 2026-09-23: "the table needs to
|
// ★ shellH 720 -> 602 so the table OPENS at seven data rows by default. 602 = map 360 + bar 32 + 7 rows x 26 + foot 28.
|
||||||
// only show SEVEN rows by DEFAULT height"). 602 = map 360 + bar 32 + 7 rows x 26 + foot 28.
|
|
||||||
// ★ A DEFAULT, NOT A LOCK — and the distinction is the whole correction. My first attempt hard-capped
|
// ★ A DEFAULT, NOT A LOCK — and the distinction is the whole correction. My first attempt hard-capped
|
||||||
// the keyhole at 7 rows in CSS, which read as the same thing and was not: nothing writes
|
// the keyhole at 7 rows in CSS, which read as the same thing and was not: nothing writes
|
||||||
// --rd-table-rows, while the drag handles write --rd-shell-h and --rd-map-h, so the cap could not
|
// --rd-table-rows, while the drag handles write --rd-shell-h and --rd-map-h, so the cap could not
|
||||||
// be reached by any gesture. The Driver lost BOTH resizes at once and said so: "I didn't want the
|
// be reached by any gesture: BOTH resizes were lost at once, when only the table was meant to
|
||||||
// table set to 7. Only the table needs to be resizable. We've set it to where I can't even resize."
|
// change (seven rows as a default, still resizable). Setting the STARTING VALUE instead leaves the
|
||||||
// Setting the STARTING VALUE instead leaves the derivation intact: the keyhole stays
|
// derivation intact: the keyhole stays
|
||||||
// shell - map - bar - foot, so dragging either handle still moves the table, and seven rows is
|
// shell - map - bar - foot, so dragging either handle still moves the table, and seven rows is
|
||||||
// merely where it opens.
|
// merely where it opens.
|
||||||
// ★ RECLAIM PASS 2 (Driver 2026-09-23: "it is a priority to reclaim height... so I can see half
|
// ★ RECLAIM PASS 2: reclaim height so half of the People Groups grid's first row shows. mapH 360 -> 300 and shellH follows the same
|
||||||
// of the first row of the people groups grid"). mapH 360 -> 300 and shellH follows the same
|
|
||||||
// arithmetic as before: 300 map + 32 bar + 7x26 rows + 28 foot = 542. The seven-row table
|
// arithmetic as before: 300 map + 32 bar + 7x26 rows + 28 foot = 542. The seven-row table
|
||||||
// promise is unchanged; only the map's share shrinks -- and the map compensates by DEFAULTING
|
// promise is unchanged; only the map's share shrinks -- and the map compensates by DEFAULTING
|
||||||
// MORE ZOOMED OUT with the world-floor released on the globe (see DashboardMapPanel), so a
|
// MORE ZOOMED OUT with the world-floor released on the globe (see DashboardMapPanel), so a
|
||||||
// shorter map shows MORE world, not less.
|
// shorter map shows MORE world, not less.
|
||||||
// shellH 502 -> 542 (Driver 2026-09-23: "the dashboard should be taller by about 40px"). The
|
// shellH 502 -> 542: the dashboard about 40px taller. The
|
||||||
// extra 40px flows to the TABLE via the standing derivation (table = shell - map - bar - foot);
|
// extra 40px flows to the TABLE via the standing derivation (table = shell - map - bar - foot);
|
||||||
// the map row stays pinned at mapH. One owner: this number, with the CSS var fallbacks in
|
// the map row stays pinned at mapH. One owner: this number, with the CSS var fallbacks in
|
||||||
// research-dashboard.vue kept byte-equal at 542.
|
// research-dashboard.vue kept byte-equal at 542.
|
||||||
// mapH 260 -> 300 (Driver 2026-09-23 follow-up: the +40px was to be "added to the map -- map is
|
// mapH 260 -> 300: the +40px belongs to the map, 40px more display room. shellH stays 542, so the table's derived share returns to its
|
||||||
// 40px taller display room"). shellH stays 542, so the table's derived share returns to its
|
|
||||||
// pre-+40 height and the whole gain lands in the map row.
|
// pre-+40 height and the whole gain lands in the map row.
|
||||||
// +40px again (Driver 2026-09-23 second raise: "go taller with the dashboard by another 40px,
|
// +40px again, added to the map's height: shellH 542 -> 582 and mapH 300 -> 340 move together, so
|
||||||
// add that to the height of the map"): shellH 542 -> 582 and mapH 300 -> 340 move together, so
|
|
||||||
// the whole gain lands in the map row and the table's derived share stays put.
|
// the whole gain lands in the map row and the table's derived share stays put.
|
||||||
// Driver fine-tune 2026-09-23: "almost perfect height -- keep the map the same height, but
|
// Fine-tune: the map keeps its height and the dashboard loses about 20px. Map stays 340; the 20px comes out of
|
||||||
// decrease the height of the dashboard by about 20px". Map stays 340; the 20px comes out of
|
|
||||||
// the table's derived share (shell - map - bar - foot).
|
// the table's derived share (shell - map - bar - foot).
|
||||||
const DEFAULTS = { facetW: 296, railW: 296, tableH: '34%', mapH: 340, shellH: 562 }
|
const DEFAULTS = { facetW: 296, railW: 296, tableH: '34%', mapH: 340, shellH: 562 }
|
||||||
const CLAMPS = { facetW: [160, 420], railW: [200, 520], tableH: [12, 70], mapH: [220, 1400], shellH: [400, 2600] }
|
const CLAMPS = { facetW: [160, 420], railW: [200, 520], tableH: [12, 70], mapH: [220, 1400], shellH: [400, 2600] }
|
||||||
|
|
@ -157,16 +151,16 @@ export function useDashboardLayout(options = {}) {
|
||||||
bus = null,
|
bus = null,
|
||||||
isNarrow = false,
|
isNarrow = false,
|
||||||
pageScroll = false,
|
pageScroll = false,
|
||||||
// ★ KEY VERSIONED v2 (2026-09-23). The Driver reported "sidebars are not equally spaced" on
|
// ★ KEY VERSIONED v2. The sidebars still looked unequal AFTER the symmetric-rail change landed
|
||||||
// his machine AFTER the symmetric-rail change landed -- because his browser restores a layout
|
// -- because a browser restores a layout
|
||||||
// SAVED under the old defaults (rail 264), and a restored value outranks any new default by
|
// SAVED under the old defaults (rail 264), and a restored value outranks any new default by
|
||||||
// design. When defaults change materially, the old saved layouts are prescriptions from a
|
// design. When defaults change materially, the old saved layouts are prescriptions from a
|
||||||
// different bottle: bump the key so every reader starts from the new defaults once, and a
|
// different bottle: bump the key so every reader starts from the new defaults once, and a
|
||||||
// fresh drag re-personalises from there. The v1 blob is left in place, inert.
|
// fresh drag re-personalises from there. The v1 blob is left in place, inert.
|
||||||
// v2 -> v3: retire stored layouts so the new 542 default is actually SEEN -- a persisted
|
// v2 -> v3: retire stored layouts so the new 542 default is actually SEEN -- a persisted
|
||||||
// shellH:502 from the previous default would silently mask this change on the Driver's browser.
|
// shellH:502 from the previous default would silently mask this change in any browser that saved one.
|
||||||
// ★ NAMESPACED PER DASHBOARD, AND THAT IS THE ONE BEHAVIOUR CHANGE IN THIS MOVE.
|
// ★ NAMESPACED PER DASHBOARD, AND THAT IS THE ONE BEHAVIOUR CHANGE IN THIS MOVE.
|
||||||
// SPEC-DASHBOARD-KIT section 8 named it as landing 2's risk: this key was a single constant, so
|
// This was the move's named risk: the key was a single constant, so
|
||||||
// the moment a page carries TWO dashboards they read and write one another's layout. Dragging a
|
// the moment a page carries TWO dashboards they read and write one another's layout. Dragging a
|
||||||
// divider in one would move the other's on next load, and neither would be wrong on its own.
|
// divider in one would move the other's on next load, and neither would be wrong on its own.
|
||||||
// While there was exactly one dashboard that was invisible; sharing the engine is precisely
|
// While there was exactly one dashboard that was invisible; sharing the engine is precisely
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,8 @@ export function buildSearchAccessors(config) {
|
||||||
* array from buildSearchAccessors(config). Case-insensitive substring over each accessor's value.
|
* array from buildSearchAccessors(config). Case-insensitive substring over each accessor's value.
|
||||||
*/
|
*/
|
||||||
export function makeSearchMatcher(bus, accessors) {
|
export function makeSearchMatcher(bus, accessors) {
|
||||||
// ★ FUZZY, NOT SUBSTRING (Driver 2026-09-23: "add an n to the end of a country name" must
|
// ★ FUZZY, NOT SUBSTRING: a country name with an n added must still find it (matching what
|
||||||
// still find it -- check what DX-life's search bar bakes in). DX-life runs real Fuse.js with
|
// DX-life's search bar bakes in). DX-life runs real Fuse.js with
|
||||||
// { threshold: 0.4, ignoreLocation: true, minMatchCharLength: 3 } (UupgsList.vue:288), so its
|
// { threshold: 0.4, ignoreLocation: true, minMatchCharLength: 3 } (UupgsList.vue:288), so its
|
||||||
// search tolerates typos. This matcher was a plain .includes() -- the one search surface that
|
// search tolerates typos. This matcher was a plain .includes() -- the one search surface that
|
||||||
// silently lost that behaviour in the migration (measured: 'Bhutan' 13 rows, 'Bhutann' 0).
|
// silently lost that behaviour in the migration (measured: 'Bhutan' 13 rows, 'Bhutann' 0).
|
||||||
|
|
@ -75,8 +75,8 @@ export function makeSearchMatcher(bus, accessors) {
|
||||||
/**
|
/**
|
||||||
* Apply the TIERED search to a row set:
|
* Apply the TIERED search to a row set:
|
||||||
* 1. exact substring -- an exact term returns exactly the rows the equivalent facet selects.
|
* 1. exact substring -- an exact term returns exactly the rows the equivalent facet selects.
|
||||||
* 2. facet-VALUE resolution (Driver: "Austrian or Canadian... a selection of that country --
|
* 2. facet-VALUE resolution (Austrian or Canadian selects that country, and the table under the
|
||||||
* the table under the map shows the result for that country"): the term is resolved by
|
* map shows its result): the term is resolved by
|
||||||
* whole-value edit distance against each accessor's DISTINCT values, and the rows returned
|
* whole-value edit distance against each accessor's DISTINCT values, and the rows returned
|
||||||
* are those carrying a resolved value -- byte-identical to picking it as a filter.
|
* are those carrying a resolved value -- byte-identical to picking it as a filter.
|
||||||
* 3. the generic windowed fuzzy rescue, only when nothing resolves.
|
* 3. the generic windowed fuzzy rescue, only when nothing resolves.
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export const BEBAS_NEUE_FACES = [
|
||||||
const POPPINS_LATIN = 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD'
|
const POPPINS_LATIN = 'U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD'
|
||||||
const POPPINS_LATIN_EXT = 'U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF'
|
const POPPINS_LATIN_EXT = 'U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF'
|
||||||
// FIGTREE — the free stand-in for Brandon Grotesque, which is COMMERCIAL and must not ship in a
|
// FIGTREE — the free stand-in for Brandon Grotesque, which is COMMERCIAL and must not ship in a
|
||||||
// public giveaway (Driver decision 2026-09-24). Chosen by measurement: at 500/19.5312px it renders
|
// public giveaway. Chosen by measurement: at 500/19.5312px it renders
|
||||||
// their button text at 126px against Brandon's 128px on live doxa.life, the closest of 19 libre
|
// their button text at 126px against Brandon's 128px on live doxa.life, the closest of 19 libre
|
||||||
// grotesques; Inter was +6 and Archivo +7. Weight 500 only — that is the single weight the two
|
// grotesques; Inter was +6 and Archivo +7. Weight 500 only — that is the single weight the two
|
||||||
// rules that named Brandon actually use.
|
// rules that named Brandon actually use.
|
||||||
|
|
|
||||||
|
|
@ -306,8 +306,8 @@ export function useMapFly(options = {}) {
|
||||||
// FLY_CONFIG carries `speed` and `curve`, which flyTo understands and fitBounds IGNORES —
|
// FLY_CONFIG carries `speed` and `curve`, which flyTo understands and fitBounds IGNORES —
|
||||||
// so this call looked animated and was a hard cut. MEASURED in the cap-4: selecting a row
|
// so this call looked animated and was a hard cut. MEASURED in the cap-4: selecting a row
|
||||||
// moved the camera 1.45 -> 6 with only TWO distinct zoom values across 14 samples over
|
// moved the camera 1.45 -> 6 with only TWO distinct zoom values across 14 samples over
|
||||||
// ~1.7s, where an eased move shows a curve of intermediates. That is the Driver's
|
// ~1.7s, where an eased move shows a curve of intermediates. That is why transitions
|
||||||
// "transitions too snappy, not smooth" report, and it is specific to fitBounds: the flyTo
|
// read as snappy rather than smooth, and it is specific to fitBounds: the flyTo
|
||||||
// paths in this same file were always eased.
|
// paths in this same file were always eased.
|
||||||
//
|
//
|
||||||
// 900ms matches the feel of the flyTo paths at speed 0.5 without making a selection feel
|
// 900ms matches the feel of the flyTo paths at speed 0.5 without making a selection feel
|
||||||
|
|
|
||||||
|
|
@ -131,8 +131,8 @@ export function engineGlobal(provider) {
|
||||||
* position:STATIC where Mapbox declares it absolute, .mapboxgl-canary at display:block (that
|
* position:STATIC where Mapbox declares it absolute, .mapboxgl-canary at display:block (that
|
||||||
* canary is Mapbox's OWN unstyled-detector), Mapbox's "missing CSS declarations" warning firing,
|
* canary is Mapbox's OWN unstyled-detector), Mapbox's "missing CSS declarations" warning firing,
|
||||||
* and 0 markers — because a canvas container with no positioning context gives every
|
* and 0 markers — because a canvas container with no positioning context gives every
|
||||||
* absolutely-positioned child, .mapboxgl-marker included, nothing to anchor to. The Driver saw it
|
* absolutely-positioned child, .mapboxgl-marker included, nothing to anchor to. It showed as the
|
||||||
* as "the polygons and map colors and pins are not showing up".
|
* polygons, the map colours and the pins all missing.
|
||||||
*
|
*
|
||||||
* Returns null for an engine with no stylesheet, so callers can skip injection rather than
|
* Returns null for an engine with no stylesheet, so callers can skip injection rather than
|
||||||
* inject undefined.
|
* inject undefined.
|
||||||
|
|
@ -245,7 +245,7 @@ export async function ensureEngineCSS(element, cssUrl) {
|
||||||
* back the raw `mapbox://styles/mapbox/standard` URL and the DARKNESS lives in the config
|
* back the raw `mapbox://styles/mapbox/standard` URL and the DARKNESS lives in the config
|
||||||
* ({ basemap: { lightPreset: 'night' } }), which is a Mapbox-only mechanism. This function used to
|
* ({ basemap: { lightPreset: 'night' } }), which is a Mapbox-only mechanism. This function used to
|
||||||
* collapse every mapbox:// style to ONE light constant, so flipping provider turned the research
|
* collapse every mapbox:// style to ONE light constant, so flipping provider turned the research
|
||||||
* map from the dark map the Driver knows into a LIGHT one — MEASURED by screenshotting it, not by
|
* map from its familiar dark look into a LIGHT one — MEASURED by screenshotting it, not by
|
||||||
* any error: both maps mount, both paint, both report success. Interoperability had "worked" while
|
* any error: both maps mount, both paint, both report success. Interoperability had "worked" while
|
||||||
* the product looked like a different product.
|
* the product looked like a different product.
|
||||||
* Reading the preset the Mapbox side would have applied keeps the two engines telling the same
|
* Reading the preset the Mapbox side would have applied keeps the two engines telling the same
|
||||||
|
|
@ -646,8 +646,8 @@ export function useMapInstance({
|
||||||
|
|
||||||
// ── CONSOLE ONLY — NO ON-SCREEN BANNER ──────────────────────────────────────────
|
// ── CONSOLE ONLY — NO ON-SCREEN BANNER ──────────────────────────────────────────
|
||||||
// An earlier version of this handler also appended a `.dx-tile-auth-note` corner
|
// An earlier version of this handler also appended a `.dx-tile-auth-note` corner
|
||||||
// banner to the map container. REMOVED on Driver instruction 2026-09-22: "the map
|
// banner to the map container. REMOVED: the map must not show a popup saying the
|
||||||
// should not have a popup that says basemap tiles blocked".
|
// basemap tiles are blocked.
|
||||||
//
|
//
|
||||||
// WHY THE INSTRUCTION IS RIGHT, so nobody re-adds it. The banner reported an
|
// WHY THE INSTRUCTION IS RIGHT, so nobody re-adds it. The banner reported an
|
||||||
// OPERATOR problem (one account setting, on one deployment origin) to the END USER,
|
// OPERATOR problem (one account setting, on one deployment origin) to the END USER,
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,8 @@ export function hasMapToken(cfg) {
|
||||||
*/
|
*/
|
||||||
export function resolveMapProvider(profileConfig, { token } = {}) {
|
export function resolveMapProvider(profileConfig, { token } = {}) {
|
||||||
const cfg = unwrapConfig(profileConfig)
|
const cfg = unwrapConfig(profileConfig)
|
||||||
// ★ AUTO IS MAPLIBRE. A TOKEN NO LONGER CHOOSES AN ENGINE (Driver, 2026-09-28: MapLibre is the
|
// ★ AUTO IS MAPLIBRE. A TOKEN NO LONGER CHOOSES AN ENGINE (2026-09-28: MapLibre is the default
|
||||||
// default everywhere, on cost and on how it looks; Mapbox stays supported as an explicit choice).
|
// everywhere, on cost and on how it looks; Mapbox stays supported as an explicit choice).
|
||||||
//
|
//
|
||||||
// It used to read: explicit || (token present ? 'mapbox' : 'maplibre') — so merely HOLDING a
|
// It used to read: explicit || (token present ? 'mapbox' : 'maplibre') — so merely HOLDING a
|
||||||
// Mapbox token selected Mapbox. That conflated two different things: "I have a token" is a
|
// Mapbox token selected Mapbox. That conflated two different things: "I have a token" is a
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@
|
||||||
//
|
//
|
||||||
// ⚠ PRINTS IN PLACE, never window.open(). A popup is blockable and drops the reader
|
// ⚠ PRINTS IN PLACE, never window.open(). A popup is blockable and drops the reader
|
||||||
// out of the map; the shared openPrintFrame mounts a hidden same-page iframe. This
|
// out of the map; the shared openPrintFrame mounts a hidden same-page iframe. This
|
||||||
// is the mechanism the Driver asked for by name: "not supposed to open up a new
|
// is the mechanism asked for by name: no new browser window, the print dialog
|
||||||
// browser, but should open up the print dialog directly in the browser".
|
// opening directly in the page.
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { openPrintFrame } from './printInPlace.js';
|
import { openPrintFrame } from './printInPlace.js';
|
||||||
|
|
@ -98,8 +98,8 @@ export function usePosterPrints(opts = {}) {
|
||||||
// PosterWall.vue, which meant the MAP LEGEND's own "print this list's poster" had no way to
|
// PosterWall.vue, which meant the MAP LEGEND's own "print this list's poster" had no way to
|
||||||
// reach it — and the legend had grown a fourth implementation instead: a hand-rolled
|
// reach it — and the legend had grown a fourth implementation instead: a hand-rolled
|
||||||
// `<a download>` of the .webp. That was wrong twice over. It silently downloads a file,
|
// `<a download>` of the .webp. That was wrong twice over. It silently downloads a file,
|
||||||
// which the Driver ruled out explicitly (2026-09-23: "no silent file download as the default
|
// which is ruled out explicitly (no silent file download as the default action), and it has
|
||||||
// action"), and it has NO fallback, so on the 27 lists whose .webp does not ship it simply
|
// NO fallback, so on the 27 lists whose .webp does not ship it simply
|
||||||
// saved a 404.
|
// saved a 404.
|
||||||
//
|
//
|
||||||
// Two guards below were each learned the hard way and MUST survive any future move:
|
// Two guards below were each learned the hard way and MUST survive any future move:
|
||||||
|
|
@ -163,8 +163,8 @@ export function usePosterPrints(opts = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 2. THE LIST ───────────────────────────────────────────────────────────
|
// ── 2. THE LIST ───────────────────────────────────────────────────────────
|
||||||
// The Driver's FIRST option — "that's what the whole app was about": the list
|
// The FIRST option, and what the whole app is about: the list itself as a
|
||||||
// itself as a print-paginated table with a prayer checkbox per row.
|
// print-paginated table with a prayer checkbox per row.
|
||||||
async function printList(item, { columns, title } = {}) {
|
async function printList(item, { columns, title } = {}) {
|
||||||
if (busy()) return;
|
if (busy()) return;
|
||||||
const kind = 'List';
|
const kind = 'List';
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,8 @@ export function createPplrInstance(instanceId) {
|
||||||
* that shares an instance store the `defaultTab` prop was DEAD — declared, passed,
|
* that shares an instance store the `defaultTab` prop was DEAD — declared, passed,
|
||||||
* seeding a local ref, and named in a watch fallback, yet never able to win.
|
* seeding a local ref, and named in a watch fallback, yet never able to win.
|
||||||
* MEASURED 2026-09-24: my-upg-100-list passes :default-tab="1" (the UPG-100 Lists tab)
|
* MEASURED 2026-09-24: my-upg-100-list passes :default-tab="1" (the UPG-100 Lists tab)
|
||||||
* and opened on Region at BOTH widths, which is the Driver's first complaint about this
|
* and opened on Region at BOTH widths, which was the first complaint about this legend:
|
||||||
* legend verbatim — "it shouldn't have me sitting there on Regions, I don't know what to
|
* opening on Regions left the reader not knowing what to do.
|
||||||
* do, it's confusing."
|
|
||||||
* A sentinel rather than a different initial value: changing activeTab's default would
|
* A sentinel rather than a different initial value: changing activeTab's default would
|
||||||
* move every other profile's landing tab as a side effect.
|
* move every other profile's landing tab as a side effect.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ export function createSelectionBus() {
|
||||||
// ★ HOVER IS NOT SELECTION, AND MUST NEVER BECOME IT. It does not enter selectionSet, does not
|
// ★ HOVER IS NOT SELECTION, AND MUST NEVER BECOME IT. It does not enter selectionSet, does not
|
||||||
// bump selectionVersion, and therefore does not move the camera: useSelectionCamera watches the
|
// bump selectionVersion, and therefore does not move the camera: useSelectionCamera watches the
|
||||||
// version, and a camera that chased the pointer across a table would be the resize-strobe bug
|
// version, and a camera that chased the pointer across a table would be the resize-strobe bug
|
||||||
// wearing a new hat, on a Driver who is photosensitive. A panel may fade or outline on hover; it
|
// wearing a new hat, on a reader who may be photosensitive. A panel may fade or outline on hover; it
|
||||||
// may not fly, select or filter.
|
// may not fly, select or filter.
|
||||||
const hoverKey = ref(null)
|
const hoverKey = ref(null)
|
||||||
const subs = new Map() // event -> Set<cb>
|
const subs = new Map() // event -> Set<cb>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* decoupling the bus exists for — the table panel owns no camera code, and this file owns
|
* decoupling the bus exists for — the table panel owns no camera code, and this file owns
|
||||||
* no table code.
|
* no table code.
|
||||||
*
|
*
|
||||||
* ── MOTION SAFETY (BUG-RESIZE-STROBE class; the Driver is photosensitive) ──────────────
|
* ── MOTION SAFETY (BUG-RESIZE-STROBE class; a reader may be photosensitive) ────────────
|
||||||
* The strobe bug fixed in @map/composables/useMapInstance.js was caused by a resize handler
|
* The strobe bug fixed in @map/composables/useMapInstance.js was caused by a resize handler
|
||||||
* re-snapping the camera on EVERY frame at 60fps. The rule learned there is applied here:
|
* re-snapping the camera on EVERY frame at 60fps. The rule learned there is applied here:
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* useSharedMapToken.js — ONE page, ONE map token, shared by every element on it.
|
* useSharedMapToken.js — ONE page, ONE map token, shared by every element on it.
|
||||||
*
|
*
|
||||||
* THE BUG THIS ENDS (Driver, 2026-09-23; the repo already names it
|
* THE BUG THIS ENDS (the repo names it BUG-COMPOSITE-LEAVES-ARE-TOKEN-FREE-BUT-NEST-REAL-MAPS): the
|
||||||
* BUG-COMPOSITE-LEAVES-ARE-TOKEN-FREE-BUT-NEST-REAL-MAPS):
|
* element receives the tk, and it must not gatekeep it to one component — every component in a
|
||||||
* "the element gets the tk to the element itself and does not need to only gatekeep that to one
|
* plugin can share it.
|
||||||
* component only — all components in a plugin can share that tk"
|
|
||||||
*
|
*
|
||||||
* Inside ONE element that was already true: ProfileLoader provides 'mapboxToken' and eleven
|
* Inside ONE element that was already true: ProfileLoader provides 'mapboxToken' and eleven
|
||||||
* components inject it. The gate is the ELEMENT BOUNDARY. Every custom element mounts its own Vue
|
* components inject it. The gate is the ELEMENT BOUNDARY. Every custom element mounts its own Vue
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ import { provideOwn, withoutInheritedProvides } from './provide-own.js'
|
||||||
* runtime-core's app.provide() then warns on every key that is already reachable —
|
* runtime-core's app.provide() then warns on every key that is already reachable —
|
||||||
* 'App already provides property with key "profileModules" inherited from its parent element.
|
* 'App already provides property with key "profileModules" inherited from its parent element.
|
||||||
* It will be overwritten with the new value.'
|
* It will be overwritten with the new value.'
|
||||||
* — 8 of those per key, 24 in the Driver's console (2026-09-28), for three keys where overwriting is
|
* — 8 of those per key, 24 in a live console (2026-09-28), for three keys where overwriting is
|
||||||
* the POINT. `profileModules` is the plugin's own .vue glob and a child must not render its parent's
|
* the POINT. `profileModules` is the plugin's own .vue glob and a child must not render its parent's
|
||||||
* components; pinia is per-instance so two embeds never bleed state; and the i18n instance is what
|
* components; pinia is per-instance so two embeds never bleed state; and the i18n instance is what
|
||||||
* ProfileLoader drives from THIS element's profile-config, which is the whole of the permanently-
|
* ProfileLoader drives from THIS element's profile-config, which is the whole of the permanently-
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
* Vue. Same split as library/composables/host-token-url.js and library/packets/describeGroup.js.
|
* Vue. Same split as library/composables/host-token-url.js and library/packets/describeGroup.js.
|
||||||
*
|
*
|
||||||
* ★ WHY SHADOWING IS CORRECT HERE. Vue's CE layer lets a nested custom element inherit its parent
|
* ★ WHY SHADOWING IS CORRECT HERE. Vue's CE layer lets a nested custom element inherit its parent
|
||||||
* element's provides, and app.provide() warns on any key already reachable — 24 of those in the
|
* element's provides, and app.provide() warns on any key already reachable — 24 of those in a live
|
||||||
* Driver's console (2026-09-28), 8 each for profileModules, appI18n and pinia's symbol. All three must
|
* console (2026-09-28), 8 each for profileModules, appI18n and pinia's symbol. All three must
|
||||||
* be the child's OWN: profileModules is the plugin's own .vue glob, pinia is per-instance so two embeds
|
* be the child's OWN: profileModules is the plugin's own .vue glob, pinia is per-instance so two embeds
|
||||||
* never bleed state, and the i18n instance is what ProfileLoader drives from THIS element's
|
* never bleed state, and the i18n instance is what ProfileLoader drives from THIS element's
|
||||||
* profile-config. Inheriting any of them would be the bug. The warning is Vue reporting an intentional
|
* profile-config. Inheriting any of them would be the bug. The warning is Vue reporting an intentional
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ export function createAppI18n() {
|
||||||
if (RTL_LOCALES.has(locale)) document.documentElement.dir = 'rtl'
|
if (RTL_LOCALES.has(locale)) document.documentElement.dir = 'rtl'
|
||||||
}
|
}
|
||||||
} catch { /* no document (SSR/tests) — nothing to reflect */ }
|
} catch { /* no document (SSR/tests) — nothing to reflect */ }
|
||||||
// ★ A DETACHED EFFECT SCOPE (P0, the Driver's console, 2026-09-27). vue-i18n's createGlobal() runs
|
// ★ A DETACHED EFFECT SCOPE (P0, seen in a live console, 2026-09-27). vue-i18n's createGlobal() runs
|
||||||
// inside scope.run() and throws UNEXPECTED_ERROR ("SyntaxError: Unexpected error") when that returns
|
// inside scope.run() and throws UNEXPECTED_ERROR ("SyntaxError: Unexpected error") when that returns
|
||||||
// undefined; Vue 3.5 creates a new EffectScope INACTIVE when the currently active scope is STOPPED.
|
// undefined; Vue 3.5 creates a new EffectScope INACTIVE when the currently active scope is STOPPED.
|
||||||
// A custom element's connectedCallback -> configureApp -> here can run while a released tab's stopped
|
// A custom element's connectedCallback -> configureApp -> here can run while a released tab's stopped
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
everything, every section's element mounts on FIRST RENDER (not on activation). If you only want the
|
everything, every section's element mounts on FIRST RENDER (not on activation). If you only want the
|
||||||
visible ones to load, that's the tabs layout's job; a scroll-stack shows all.
|
visible ones to load, that's the tabs layout's job; a scroll-stack shows all.
|
||||||
|
|
||||||
★ THE LAYOUT OWNS THE SCROLL — the elements do NOT. This is the Driver's scroll-page contract: a
|
★ THE LAYOUT OWNS THE SCROLL — the elements do NOT. This is the scroll-page contract: a
|
||||||
plugin dropped in a section only needs ROOM to display at full height; it never needs its own
|
plugin dropped in a section only needs ROOM to display at full height; it never needs its own
|
||||||
scrollbar. The root is `display:flow-root` with NO fixed height and NO `position:absolute; inset:0`
|
scrollbar. The root is `display:flow-root` with NO fixed height and NO `position:absolute; inset:0`
|
||||||
— so it takes its content's natural height and the PARENT PAGE scrolls through everything. Each
|
— so it takes its content's natural height and the PARENT PAGE scrolls through everything. Each
|
||||||
|
|
@ -83,7 +83,7 @@ import { bundleTreeRoot } from '@map/utils/embedBase.js'
|
||||||
import { loadScriptOnce, claimTag } from '@map/utils/embedScript.js'
|
import { loadScriptOnce, claimTag } from '@map/utils/embedScript.js'
|
||||||
|
|
||||||
// ★ ANCHOR A SIBLING-BUNDLE src TO THE TREE ROOT, NOT TO A GUESSED DEPTH.
|
// ★ ANCHOR A SIBLING-BUNDLE src TO THE TREE ROOT, NOT TO A GUESSED DEPTH.
|
||||||
// THE BUG (Driver-reported 2026-09-23, verbatim console text):
|
// THE BUG (reported, the console text verbatim):
|
||||||
// Could not load <doxa-research-map> from ../../doxa-research-map/doxa-research-map.js
|
// Could not load <doxa-research-map> from ../../doxa-research-map/doxa-research-map.js
|
||||||
// A composite declares its children as `src: '../../<bundle>/<bundle>.js'`. That literal is correct
|
// A composite declares its children as `src: '../../<bundle>/<bundle>.js'`. That literal is correct
|
||||||
// from a ONE-deep leaf and wrong from a TWO-deep one, and the generator emits both:
|
// from a ONE-deep leaf and wrong from a TWO-deep one, and the generator emits both:
|
||||||
|
|
@ -110,7 +110,7 @@ function resolveBundleSrc(url) {
|
||||||
const m = new RegExp('^(.*/' + TREE_SEGMENT + '/)').exec(window.location.pathname)
|
const m = new RegExp('^(.*/' + TREE_SEGMENT + '/)').exec(window.location.pathname)
|
||||||
if (m) return m[1] + bare
|
if (m) return m[1] + bare
|
||||||
// (2) THE DOCUMENT IS NOT INSIDE THE TREE AT ALL, so there is no segment to anchor on.
|
// (2) THE DOCUMENT IS NOT INSIDE THE TREE AT ALL, so there is no segment to anchor on.
|
||||||
// Driver-reported 2026-09-23: <plugin-studio> mounts at the SERVER ROOT (/index.html) in both
|
// Reported: <plugin-studio> mounts at the SERVER ROOT (/index.html) in both
|
||||||
// trees, and from there this function's only anchor was missing, so it returned the literal
|
// trees, and from there this function's only anchor was missing, so it returned the literal
|
||||||
// '../../doxa-research-map/doxa-research-map.js' unchanged. The browser resolved that against
|
// '../../doxa-research-map/doxa-research-map.js' unchanged. The browser resolved that against
|
||||||
// '/' — where '../' cannot climb — and requested /doxa-research-map/doxa-research-map.js:
|
// '/' — where '../' cannot climb — and requested /doxa-research-map/doxa-research-map.js:
|
||||||
|
|
@ -258,7 +258,7 @@ export default defineComponent({
|
||||||
host.innerHTML = ''
|
host.innerHTML = ''
|
||||||
host.appendChild(el)
|
host.appendChild(el)
|
||||||
})
|
})
|
||||||
// ★ A FAILED SECTION MUST SAY SO (BUG-SIMPLE-RESEARCH-GREY-PANE, Driver reported 4x).
|
// ★ A FAILED SECTION MUST SAY SO (BUG-SIMPLE-RESEARCH-GREY-PANE, reported four times).
|
||||||
// The rest of the stack still renders — that part was always right, and is why this catch
|
// The rest of the stack still renders — that part was always right, and is why this catch
|
||||||
// is per-section rather than around the whole loop. What was wrong is that it left the host
|
// is per-section rather than around the whole loop. What was wrong is that it left the host
|
||||||
// EMPTY and silent, so a composite whose children all failed rendered as a blank rectangle
|
// EMPTY and silent, so a composite whose children all failed rendered as a blank rectangle
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@
|
||||||
|
|
||||||
<!-- ★ HEADERLESS MOBILE ESCAPE HATCH — the ONLY way to reach the rail when there is no header.
|
<!-- ★ HEADERLESS MOBILE ESCAPE HATCH — the ONLY way to reach the rail when there is no header.
|
||||||
FOUND 2026-09-22 by resizing the cap-5 SPA to a phone width. That SPA is header-OFF by
|
FOUND 2026-09-22 by resizing the cap-5 SPA to a phone width. That SPA is header-OFF by
|
||||||
default (the Driver's no-stacked-headers rule), so the toggle above — which lives inside
|
default (the no-stacked-headers rule), so the toggle above — which lives inside
|
||||||
`v-if="header"` — does not exist. On mobile the rail is an off-canvas drawer at
|
`v-if="header"` — does not exist. On mobile the rail is an off-canvas drawer at
|
||||||
translateX(-100%) with `navOpen` starting false, and the only other toggle (.vtl-collapse)
|
translateX(-100%) with `navOpen` starting false, and the only other toggle (.vtl-collapse)
|
||||||
sits INSIDE the drawer, measured off-screen at x:-244. Net effect: every tab except the one
|
sits INSIDE the drawer, measured off-screen at x:-244. Net effect: every tab except the one
|
||||||
|
|
@ -168,7 +168,7 @@
|
||||||
v-if="grp.folder" type="button" class="vtl-tier vtl-tier-btn"
|
v-if="grp.folder" type="button" class="vtl-tier vtl-tier-btn"
|
||||||
:aria-expanded="String(!isFolderClosed(grp))"
|
:aria-expanded="String(!isFolderClosed(grp))"
|
||||||
@click="toggleFolder(grp.key)">
|
@click="toggleFolder(grp.key)">
|
||||||
<!-- NO section-header logo (Driver rule: logos only on the vertical TAB rows, never on the
|
<!-- NO section-header logo (the rule: logos only on the vertical TAB rows, never on the
|
||||||
section dividers). The folder chevron below is an affordance, not a logo, so it stays. -->
|
section dividers). The folder chevron below is an affordance, not a logo, so it stays. -->
|
||||||
<span class="vtl-cap" v-if="grp.badge">{{ grp.badge }}</span>
|
<span class="vtl-cap" v-if="grp.badge">{{ grp.badge }}</span>
|
||||||
<span class="vtl-tl">{{ grp.label }}</span>
|
<span class="vtl-tl">{{ grp.label }}</span>
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
<span class="vtl-chev" :class="{ open: !isFolderClosed(grp) }" aria-hidden="true" v-html="iconSvg('chevron')"></span>
|
<span class="vtl-chev" :class="{ open: !isFolderClosed(grp) }" aria-hidden="true" v-html="iconSvg('chevron')"></span>
|
||||||
</button>
|
</button>
|
||||||
<!-- SECTION heading = a static always-visible label (with optional badge/count). No logo here
|
<!-- SECTION heading = a static always-visible label (with optional badge/count). No logo here
|
||||||
either — section dividers are text-only (Driver rule); row icons live on the tabs below. -->
|
either — section dividers are text-only; row icons live on the tabs below. -->
|
||||||
<div v-else class="vtl-tier">
|
<div v-else class="vtl-tier">
|
||||||
<span class="vtl-cap" v-if="grp.badge">{{ grp.badge }}</span>
|
<span class="vtl-cap" v-if="grp.badge">{{ grp.badge }}</span>
|
||||||
<span class="vtl-tl">{{ grp.label }}</span>
|
<span class="vtl-tl">{{ grp.label }}</span>
|
||||||
|
|
@ -228,7 +228,7 @@
|
||||||
|
|
||||||
<main class="vtl-main">
|
<main class="vtl-main">
|
||||||
<!-- The #toolbar strip (locale switcher etc.) is part of the header chrome, so it renders ONLY
|
<!-- The #toolbar strip (locale switcher etc.) is part of the header chrome, so it renders ONLY
|
||||||
when header is on. Driver rule: a header-off, sidebar-only element (doxa-maps-spa) inherits
|
when header is on. The rule: a header-off, sidebar-only element (doxa-maps-spa) inherits
|
||||||
its language from the PARENT via the `language` prop and shows NO standalone locale dropdown;
|
its language from the PARENT via the `language` prop and shows NO standalone locale dropdown;
|
||||||
a header-on host (plugin-studio) DOES get the toolbar/locale dropdown. -->
|
a header-on host (plugin-studio) DOES get the toolbar/locale dropdown. -->
|
||||||
<slot name="toolbar" v-if="header" />
|
<slot name="toolbar" v-if="header" />
|
||||||
|
|
@ -621,7 +621,7 @@ export default defineComponent({
|
||||||
* ★ ONE SOURCE OF TRUTH FOR THE FLOATING CIRCLE, because two copies of this condition is the
|
* ★ ONE SOURCE OF TRUTH FOR THE FLOATING CIRCLE, because two copies of this condition is the
|
||||||
* bug waiting to happen. The button's v-if and the --map-fab-clearance that reserves room for
|
* bug waiting to happen. The button's v-if and the --map-fab-clearance that reserves room for
|
||||||
* it MUST agree: if they drift, you get either a legend whose last row sits under the circle
|
* it MUST agree: if they drift, you get either a legend whose last row sits under the circle
|
||||||
* (the Driver's report) or a strip of dead whitespace with no circle to justify it. Neither
|
* (the reported case) or a strip of dead whitespace with no circle to justify it. Neither
|
||||||
* has a symptom a gate can see, so the fix is to make disagreement impossible rather than to
|
* has a symptom a gate can see, so the fix is to make disagreement impossible rather than to
|
||||||
* test for it.
|
* test for it.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ export function localizeBasemapLabels(map, lang) {
|
||||||
// MEASURED against the live tiles (six z3/z4 tiles across Europe, Asia and Africa, identical key
|
// MEASURED against the live tiles (six z3/z4 tiles across Europe, Asia and Africa, identical key
|
||||||
// sets): name_ar, name_de, name_el, name_en, name_es, name_fr, name_it, name_nl, name_pl,
|
// sets): name_ar, name_de, name_el, name_en, name_es, name_fr, name_it, name_nl, name_pl,
|
||||||
// name_pt, name_uk. Of the eleven locales we ship that covers SEVEN — ar, de, en, es, fr, it, pt
|
// name_pt, name_uk. Of the eleven locales we ship that covers SEVEN — ar, de, en, es, fr, it, pt
|
||||||
// — in their own language, with no change to the basemap the Driver chose.
|
// — in their own language, with no change to the chosen basemap.
|
||||||
//
|
//
|
||||||
// hi, ro, ru and zh are genuinely absent from those tiles, so they take name_en: Latin script,
|
// hi, ro, ru and zh are genuinely absent from those tiles, so they take name_en: Latin script,
|
||||||
// one script, readable rather than mixed CJK and Devanagari endonyms. That is the same role
|
// one script, readable rather than mixed CJK and Devanagari endonyms. That is the same role
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* mercator-slice.js — cut a map's bounds into EQUAL-HEIGHT rows, which is not the same as equal degrees.
|
* mercator-slice.js — cut a map's bounds into EQUAL-HEIGHT rows, which is not the same as equal degrees.
|
||||||
*
|
*
|
||||||
* ★ THE DEFECT THIS EXISTS FOR (Driver, 2026-09-28, Simple Research's poster export): the poster "shows
|
* ★ THE DEFECT THIS EXISTS FOR (Simple Research's poster export): the poster showed too much of
|
||||||
* too much of Antarctica" and comes out at the wrong, vertical aspect.
|
* Antarctica and came out at the wrong, vertical aspect.
|
||||||
*
|
*
|
||||||
* The big-poster renderer stitches a grid of tiles, each rendered into an EQUAL-HEIGHT slot in the
|
* The big-poster renderer stitches a grid of tiles, each rendered into an EQUAL-HEIGHT slot in the
|
||||||
* output image. It chose each tile's latitude band by dividing the degree span evenly:
|
* output image. It chose each tile's latitude band by dividing the degree span evenly:
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
* DEGREE bands are not equal SCREEN bands. MEASURED on a -60..75 view in 4 rows: the top band is 37.0%
|
* DEGREE bands are not equal SCREEN bands. MEASURED on a -60..75 view in 4 rows: the top band is 37.0%
|
||||||
* of the true screen height and is drawn into 25% of the poster; the next is 19.7% drawn into 25%. Every
|
* of the true screen height and is drawn into 25% of the poster; the next is 19.7% drawn into 25%. Every
|
||||||
* row is squeezed or stretched, which is the wrong aspect, and the high-latitude bands — where a degree
|
* row is squeezed or stretched, which is the wrong aspect, and the high-latitude bands — where a degree
|
||||||
* is worth the most pixels — are the most over-represented, which is the Antarctica the Driver saw.
|
* is worth the most pixels — are the most over-represented, which is where the excess Antarctica came from.
|
||||||
*
|
*
|
||||||
* Slicing in MERCATOR Y instead gives 25.0% per row exactly, because that IS the screen coordinate.
|
* Slicing in MERCATOR Y instead gives 25.0% per row exactly, because that IS the screen coordinate.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
// ★ THE POINT OF THIS FILE: A DECK PRINTS AS CARDS, NOT AS PAPER.
|
// ★ THE POINT OF THIS FILE: A DECK PRINTS AS CARDS, NOT AS PAPER.
|
||||||
// The previous deck set `@page{size:letter portrait}` and gave each card
|
// The previous deck set `@page{size:letter portrait}` and gave each card
|
||||||
// `height:100%`, so one card was stretched across a whole 8.5x11 sheet — 95
|
// `height:100%`, so one card was stretched across a whole 8.5x11 sheet — 95
|
||||||
// mostly-empty letter pages, with no photo. The Driver: "the print dialog that
|
// mostly-empty letter pages, with no photo. The print dialog has to show every
|
||||||
// opens up needs to show all the cards not in a paper format but a CARD format,
|
// card in a CARD format, not a paper format: the aspect ratio the card generator
|
||||||
// the aspect ratio that is used in the card generator."
|
// uses.
|
||||||
//
|
//
|
||||||
// So the page IS the card: @page takes the card's own dimensions, and every card
|
// So the page IS the card: @page takes the card's own dimensions, and every card
|
||||||
// fills exactly one page. Print two-sided on cardstock and cut, or print to PDF
|
// fills exactly one page. Print two-sided on cardstock and cut, or print to PDF
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
||||||
const PAGE_W_IN = 8.5;
|
const PAGE_W_IN = 8.5;
|
||||||
const SCALE = (PAGE_W_IN * 96 / W).toFixed(6); // print scale: one sheet = one page
|
const SCALE = (PAGE_W_IN * 96 / W).toFixed(6); // print scale: one sheet = one page
|
||||||
const M = 60, HEAD = 90; // margins + running header
|
const M = 60, HEAD = 90; // margins + running header
|
||||||
// ★ THE PROFILE TABLE (Driver brief A1 item 1): ~20 per page, picture | generated localized
|
// ★ THE PROFILE TABLE (brief A1, item 1): ~20 per page, picture | generated localized
|
||||||
// description | three prayer checkboxes. This replaces the 2-column card grid with 20 FULL-WIDTH
|
// description | three prayer checkboxes. This replaces the 2-column card grid with 20 FULL-WIDTH
|
||||||
// ROWS. PER_PAGE stays 20, so this is a change of LAYOUT SHAPE, NOT OF PAGINATION — which is what
|
// ROWS. PER_PAGE stays 20, so this is a change of LAYOUT SHAPE, NOT OF PAGINATION — which is what
|
||||||
// lets check-packet-render's dropped-groups assertion (sheets === 1 + … + ceil(cards/PER_PAGE) + 2)
|
// lets check-packet-render's dropped-groups assertion (sheets === 1 + … + ceil(cards/PER_PAGE) + 2)
|
||||||
|
|
@ -100,7 +100,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
||||||
// comment refuses a ±1 tolerance, so a rewrite that needed it loosened would be the wrong rewrite.
|
// comment refuses a ±1 tolerance, so a rewrite that needed it loosened would be the wrong rewrite.
|
||||||
const ROWS = 20, PER_PAGE = ROWS;
|
const ROWS = 20, PER_PAGE = ROWS;
|
||||||
const COLS = 1;
|
const COLS = 1;
|
||||||
// ★ THE THREE PRAYER COLUMNS, the Driver's own three questions (2026-09-27), in his order:
|
// ★ THE THREE PRAYER COLUMNS, three questions in this order:
|
||||||
// 1 have I blessed them
|
// 1 have I blessed them
|
||||||
// 2 God, what do you want me to KNOW about them
|
// 2 God, what do you want me to KNOW about them
|
||||||
// 3 God, what do you want me to DO to help them
|
// 3 God, what do you want me to DO to help them
|
||||||
|
|
@ -484,8 +484,8 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── PAGE 1: the map one-pager ───────────────────────────────────────────────
|
// ── PAGE 1: the map one-pager ───────────────────────────────────────────────
|
||||||
// Lightweight by design — the Driver asked for "a lightweight one pager that shows
|
// Lightweight by design — a one-page map showing the geography with all the pins,
|
||||||
// the geography with all the pins", explicitly NOT the poster. The poster remains
|
// explicitly NOT the poster. The poster remains
|
||||||
// its own artefact (generate.mjs); this is the packet's cover.
|
// its own artefact (generate.mjs); this is the packet's cover.
|
||||||
// ★ IT SAYS WHICH SET IT BELONGS TO. A partner holding one packet must be able to tell it is one of
|
// ★ IT SAYS WHICH SET IT BELONGS TO. A partner holding one packet must be able to tell it is one of
|
||||||
// the UPG 100 lists, not a stand-alone leaflet — hence the series line. The count is READ from
|
// the UPG 100 lists, not a stand-alone leaflet — hence the series line. The count is READ from
|
||||||
|
|
@ -571,7 +571,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
||||||
|
|
||||||
// ── PAGE 2: the funnel — how this list was narrowed out of every people group ─
|
// ── PAGE 2: the funnel — how this list was narrowed out of every people group ─
|
||||||
//
|
//
|
||||||
// WHY THIS PAGE EXISTS. Driver 2026-09-26, via the shepherd: the packets go to ALL partners for
|
// WHY THIS PAGE EXISTS. The packets go to ALL partners for
|
||||||
// the largest prayer campaign, and a partner holding one list needs to see where their list sits
|
// the largest prayer campaign, and a partner holding one list needs to see where their list sits
|
||||||
// in the whole. The narrowing IS the argument: 12,379 people groups on earth, 7,341 of them
|
// in the whole. The narrowing IS the argument: 12,379 people groups on earth, 7,341 of them
|
||||||
// unreached, 4,754 frontier, 2,107 on this network's list, and N of those in the reader's hand.
|
// unreached, 4,754 frontier, 2,107 on this network's list, and N of those in the reader's hand.
|
||||||
|
|
@ -593,8 +593,8 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
||||||
// ⚠ K AND N ARE READ AT RENDER TIME, NEVER TYPED. Measured 2026-09-26: the brief said "~30 lists
|
// ⚠ K AND N ARE READ AT RENDER TIME, NEVER TYPED. Measured 2026-09-26: the brief said "~30 lists
|
||||||
// of 50-100" and the catalogue says K=27 with counts from 8 to 118 — west-africa 118,
|
// of 50-100" and the catalogue says K=27 with counts from 8 to 118 — west-africa 118,
|
||||||
// asia-india-rajasthan 116, uncovered-scatter 8. Only about half sit inside 50-100, so this page
|
// asia-india-rajasthan 116, uncovered-scatter 8. Only about half sit inside 50-100, so this page
|
||||||
// never claims that band; it states the share the data can defend. Shepherd approved exactly this
|
// never claims that band; it states the share the data can defend. K=27 includes the two residual
|
||||||
// (ENDTOKEN-DXS-UPG-KDECIDE), K=27 including the two residual buckets until the Driver rules.
|
// buckets: provisional, pending a product decision.
|
||||||
//
|
//
|
||||||
// ⚠ "NUMERALS UNTOUCHED" IS ABOUT DIGITS, NOT GROUPING, and those are two properties. I verified the
|
// ⚠ "NUMERALS UNTOUCHED" IS ABOUT DIGITS, NOT GROUPING, and those are two properties. I verified the
|
||||||
// digits and assumed the rest. rega measured the rendered hi sheet and found Indian grouping —
|
// digits and assumed the rest. rega measured the rendered hi sheet and found Indian grouping —
|
||||||
|
|
@ -847,14 +847,14 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
||||||
|
|
||||||
// ── LIFE CALENDAR: 100 years x 52 weeks = 5200 boxes ────────────────────────
|
// ── LIFE CALENDAR: 100 years x 52 weeks = 5200 boxes ────────────────────────
|
||||||
//
|
//
|
||||||
// Driver, 2026-09-27: page 2 is "a life calendar of the years of a human life in weeks", and page 3
|
// Page 2 is a life calendar: the years of a human life, in weeks. Page 3 is the same calendar used
|
||||||
// is "the same life cal used for percentage of Christian on average for dx people groups". ONE grid,
|
// for the average percentage of Christians in the DOXA people groups. ONE grid,
|
||||||
// TWO readings — which is the whole point. A percentage drawn as a bar is an abstraction a reader
|
// TWO readings — which is the whole point. A percentage drawn as a bar is an abstraction a reader
|
||||||
// has to translate; drawn on the grid they have just understood as their OWN LIFE, it is the same
|
// has to translate; drawn on the grid they have just understood as their OWN LIFE, it is the same
|
||||||
// 5200 boxes they were looking at a page earlier.
|
// 5200 boxes they were looking at a page earlier.
|
||||||
//
|
//
|
||||||
// ★ 5,200 IS NOT ROUNDED OR CHOSEN FOR LAYOUT: 100 years x 52 weeks, the Driver's own numbers
|
// ★ 5,200 IS NOT ROUNDED OR CHOSEN FOR LAYOUT: 100 years x 52 weeks, a 100-year life in weeks.
|
||||||
// ("a 100 year life in weeks"). Everything else here is derived from it, so the two pages cannot
|
// Everything else here is derived from it, so the two pages cannot
|
||||||
// drift apart into two grids that merely look alike.
|
// drift apart into two grids that merely look alike.
|
||||||
// ★ LAID OUT LIKE THE PRINTED LIFE CALENDAR (the Module 1 "Life Calendar in Weeks" sheet, which the
|
// ★ LAID OUT LIKE THE PRINTED LIFE CALENDAR (the Module 1 "Life Calendar in Weeks" sheet, which the
|
||||||
// Driver asked this page to imitate): square boxes, week numbers across the top, the year down the
|
// Driver asked this page to imitate): square boxes, week numbers across the top, the year down the
|
||||||
|
|
@ -911,8 +911,8 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
||||||
|
|
||||||
// Page 3 — the SAME grid, shaded by the share of the DX population that is evangelical.
|
// Page 3 — the SAME grid, shaded by the share of the DX population that is evangelical.
|
||||||
//
|
//
|
||||||
// ⚠ THE NUMBER IS POPULATION-WEIGHTED, NOT A GROUP COUNT, because the Driver asked for
|
// ⚠ THE NUMBER IS POPULATION-WEIGHTED, NOT A GROUP COUNT, because the page shows the Christian
|
||||||
// "% of Christian population vs lost". Those differ sharply: 69.1% of the 2,091 matched DX GROUPS
|
// share of the POPULATION against the lost. Those differ sharply: 69.1% of the 2,091 matched DX GROUPS
|
||||||
// have no known evangelicals, but weighted by POPULATION the evangelical share is 0.61% of
|
// have no known evangelicals, but weighted by POPULATION the evangelical share is 0.61% of
|
||||||
// 209,079,159 people — 32 boxes of 5,200. A group-count version would have shown a different,
|
// 209,079,159 people — 32 boxes of 5,200. A group-count version would have shown a different,
|
||||||
// milder picture of the same data.
|
// milder picture of the same data.
|
||||||
|
|
@ -1110,8 +1110,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
||||||
.join(' · ');
|
.join(' · ');
|
||||||
const sub = p.sub ? ` <span style="font:italic 17px Georgia,serif;color:#8c8170">— ${esc(p.sub)}</span>` : '';
|
const sub = p.sub ? ` <span style="font:italic 17px Georgia,serif;color:#8c8170">— ${esc(p.sub)}</span>` : '';
|
||||||
const passage = p.print_passage
|
const passage = p.print_passage
|
||||||
// Point 4 prints Mark 5:1-20 IN FULL. Driver: "it actually lists out the full
|
// Point 4 prints Mark 5:1-20 IN FULL, because prayer point four IS reading the word.
|
||||||
// scripture, because prayer point number four is actually read the word."
|
|
||||||
// class="p4-passage" is a TEST HOOK, not styling — the block is styled inline.
|
// class="p4-passage" is a TEST HOOK, not styling — the block is styled inline.
|
||||||
// Without it the only way to assert the passage rendered was to search for the
|
// Without it the only way to assert the passage rendered was to search for the
|
||||||
// English word "Gadarenes", which would go red the moment this key is translated.
|
// English word "Gadarenes", which would go red the moment this key is translated.
|
||||||
|
|
@ -1371,7 +1370,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
||||||
// working unchanged and a host with no funnel data still gets a complete, correct packet. A page
|
// working unchanged and a host with no funnel data still gets a complete, correct packet. A page
|
||||||
// that cannot state true numbers must not appear at all — better a 4-section packet than one
|
// that cannot state true numbers must not appear at all — better a 4-section packet than one
|
||||||
// asserting figures it could not read.
|
// asserting figures it could not read.
|
||||||
// ★ TWO PACKETS, ONE RENDERER — the Driver's §A, 2026-09-28: the Prayer List print gains a caret
|
// ★ TWO PACKETS, ONE RENDERER: the Prayer List print gains a caret
|
||||||
// with "Full detail" and "Lightweight". They are NOT two code paths. Every page below already
|
// with "Full detail" and "Lightweight". They are NOT two code paths. Every page below already
|
||||||
// exists and is already exported; a variant is a FILTER over the page array, so the two packets
|
// exists and is already exported; a variant is a FILTER over the page array, so the two packets
|
||||||
// cannot drift apart in layout, i18n, RTL or print scale — there is only ever one implementation
|
// cannot drift apart in layout, i18n, RTL or print scale — there is only ever one implementation
|
||||||
|
|
@ -1410,7 +1409,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
||||||
// sees the same 5,200 boxes answer "how many of these people know Christ".
|
// sees the same 5,200 boxes answer "how many of these people know Christ".
|
||||||
// ⚠ KEYED, NOT INDEXED. The omit-set names pages; it never carries positions. An index-based
|
// ⚠ KEYED, NOT INDEXED. The omit-set names pages; it never carries positions. An index-based
|
||||||
// filter reads identically today and silently drops the WRONG page the first time this order
|
// filter reads identically today and silently drops the WRONG page the first time this order
|
||||||
// changes — and the order above is the Driver's, so it is expected to change again.
|
// changes — and the order above is a product decision, so it is expected to change again.
|
||||||
// An unknown variant falls back to the full packet: a typo must not silently print a shorter
|
// An unknown variant falls back to the full packet: a typo must not silently print a shorter
|
||||||
// packet, because a packet missing two pages still looks like a packet.
|
// packet, because a packet missing two pages still looks like a packet.
|
||||||
const omit = VARIANTS[variant] || VARIANTS.full;
|
const omit = VARIANTS[variant] || VARIANTS.full;
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
// browser, on click.
|
// browser, on click.
|
||||||
//
|
//
|
||||||
// ★ WHY: 378 posters were committed as .webp — 41 MB in every clone, shipped to
|
// ★ WHY: 378 posters were committed as .webp — 41 MB in every clone, shipped to
|
||||||
// every reader whether or not they open one. Driver: "it would be a big win to not
|
// every reader whether or not they open one. Not shipping ~80 MB of images in a
|
||||||
// have to have 80mb of images in a public folder and let the posters get generated
|
// public folder, and letting the website generate the posters, is a big win.
|
||||||
// by the website." A publish was ALREADY rejected once at a 74 MB asset tree
|
// A publish was ALREADY rejected once at a 74 MB asset tree
|
||||||
// (git-packed to 57 MiB), which is what not doing this costs.
|
// (git-packed to 57 MiB), which is what not doing this costs.
|
||||||
//
|
//
|
||||||
// THE MOVE IS NOT A REWRITE. generate.mjs is 2,816 lines with 26 Node touchpoints;
|
// THE MOVE IS NOT A REWRITE. generate.mjs is 2,816 lines with 26 Node touchpoints;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// qr.js — a minimal, self-contained QR encoder. No dependency, no network.
|
// qr.js — a minimal, self-contained QR encoder. No dependency, no network.
|
||||||
//
|
//
|
||||||
// WHY HAND-ROLLED. The prayer cards need a QR per group linking back to that
|
// WHY HAND-ROLLED. The prayer cards need a QR per group linking back to that
|
||||||
// group's live page, and the Driver's standing constraint is "as much data as you
|
// group's live page, and the standing constraint is as much data as possible
|
||||||
// can without compromising speed and without adding a lot of dependencies on the
|
// without compromising speed and without adding a lot of dependencies on the
|
||||||
// internet". A QR symbol is pure computation — Reed-Solomon over GF(256) and a bit
|
// internet. A QR symbol is pure computation — Reed-Solomon over GF(256) and a bit
|
||||||
// layout — so it costs one small file instead of a package, and it runs identically
|
// layout — so it costs one small file instead of a package, and it runs identically
|
||||||
// in Node (build-time decks) and the browser (print-on-click).
|
// in Node (build-time decks) and the browser (print-on-click).
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* describeGroup.js — one localized sentence pair describing a people group, built ONLY from API fields.
|
* describeGroup.js — one localized sentence pair describing a people group, built ONLY from API fields.
|
||||||
*
|
*
|
||||||
* DRIVER BRIEF 2026-09-28 §A1.5: the packet's profile rows carry "a short GENERATED description built
|
* THE BRIEF: the packet's profile rows carry a short GENERATED description built from the API
|
||||||
* from the API variables, localized: how lost/unreached they are, where (country), what language they
|
* variables, localized: how lost/unreached they are, where (country), what language they speak.
|
||||||
* speak". ~20 rows per page, so it has to be short, and it has to be right in eleven languages.
|
* ~20 rows per page, so it has to be short, and it has to be right in eleven languages.
|
||||||
*
|
*
|
||||||
* PURE. (group, locale) -> string. No refs, no map, no fetch. The strings object is the third argument
|
* PURE. (group, locale) -> string. No refs, no map, no fetch. The strings object is the third argument
|
||||||
* so a test can inject one; it defaults to the packet's own catalogue, which is what the renderer
|
* so a test can inject one; it defaults to the packet's own catalogue, which is what the renderer
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* LAYOUT IS COMPUTED HERE, ONCE, AT BUILD TIME, so a phone does none of it, and the output is projected
|
* LAYOUT IS COMPUTED HERE, ONCE, AT BUILD TIME, so a phone does none of it, and the output is projected
|
||||||
* to lng/lat so a map engine can draw it undistorted on the GPU.
|
* to lng/lat so a map engine can draw it undistorted on the GPU.
|
||||||
*
|
*
|
||||||
* ★ WHY THIS IS IN library/ AND WHAT DELIBERATELY IS NOT (Driver brief §B, "make it reusable"). This was
|
* ★ WHY THIS IS IN library/ AND WHAT DELIBERATELY IS NOT (the brief: make it reusable). This was
|
||||||
* extracted from gospel-internet's scripts/build-language-tree.mjs, which does TWO jobs in one file: this
|
* extracted from gospel-internet's scripts/build-language-tree.mjs, which does TWO jobs in one file: this
|
||||||
* layout, and a classification of each language from eBible, FCBH verse scopes and a screened link table.
|
* layout, and a classification of each language from eBible, FCBH verse scopes and a screened link table.
|
||||||
* Moving the file whole would have put those sources inside library/ and a fork building the tree from
|
* Moving the file whole would have put those sources inside library/ and a fork building the tree from
|
||||||
|
|
@ -159,8 +159,8 @@ export function buildLanguageTree({
|
||||||
// ★ HOW MUCH OF THE SQUARE THE TAPESTRY SPANS DECIDES HOW FAR A READER CAN ZOOM OUT, and that is the
|
// ★ HOW MUCH OF THE SQUARE THE TAPESTRY SPANS DECIDES HOW FAR A READER CAN ZOOM OUT, and that is the
|
||||||
// whole reason this number is a parameter with a measured default. The engines stop at zoom 0 (the
|
// whole reason this number is a parameter with a measured default. The engines stop at zoom 0 (the
|
||||||
// whole Mercator square), so the SMALLER the drawing, the more room there is below its opening view.
|
// whole Mercator square), so the SMALLER the drawing, the more room there is below its opening view.
|
||||||
// At 68% there was about one level of room (Driver, 2026-09-27: "I need to be able to zoom out a bit
|
// At 68% there was about one level of room, and zooming out a bit further was needed; at 0.17
|
||||||
// more"); at 0.17 there are two.
|
// there are two.
|
||||||
// The conversion moved to library/maps/whiteboard-projection.js so a second whiteboard chart cannot
|
// The conversion moved to library/maps/whiteboard-projection.js so a second whiteboard chart cannot
|
||||||
// re-derive it — gospel's set charts were about to scale linearly into degrees, which Mercator then
|
// re-derive it — gospel's set charts were about to scale linearly into degrees, which Mercator then
|
||||||
// renders as ovals. Behaviour here is unchanged and check:whiteboard-projection asserts that over a
|
// renders as ovals. Behaviour here is unchanged and check:whiteboard-projection asserts that over a
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
//
|
//
|
||||||
// ★ THIS IS NOT THE RESEARCH MAP. library/scripts/research/out/iso3-to-iso2.json is a different file
|
// ★ THIS IS NOT THE RESEARCH MAP. library/scripts/research/out/iso3-to-iso2.json is a different file
|
||||||
// with a different job: the 176-country spine that drives the data FETCHES. Expanding that one changes
|
// with a different job: the 176-country spine that drives the data FETCHES. Expanding that one changes
|
||||||
// what gets fetched, which is a data-scope decision for the Driver (dx-shepherd, REG242B) and must not
|
// what gets fetched, which is a product decision about data scope and must not
|
||||||
// happen as a side effect of adding country names. This map drives no fetch. The two are deliberately
|
// happen as a side effect of adding country names. This map drives no fetch. The two are deliberately
|
||||||
// not interchangeable, which is why they have different names, different shapes and this paragraph.
|
// not interchangeable, which is why they have different names, different shapes and this paragraph.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
*
|
*
|
||||||
* WHY. The map downloaded and parsed the whole slim file — every column, as one object per row — to
|
* WHY. The map downloaded and parsed the whole slim file — every column, as one object per row — to
|
||||||
* draw ten fields. That, plus waiting on the live prayer API before drawing anything, is most of why
|
* draw ten fields. That, plus waiting on the live prayer API before drawing anything, is most of why
|
||||||
* the Driver found it "not even loading, takes a very long time". This file keeps the same rows and
|
* the map seemed not to load at all, or took a very long time. This file keeps the same rows and
|
||||||
* values in column order, with the repetitive strings (country, religion, evangelical level,
|
* values in column order, with the repetitive strings (country, religion, evangelical level,
|
||||||
* language) stored once in dictionaries and referenced by index, and coordinates at 3 decimals
|
* language) stored once in dictionaries and referenced by index, and coordinates at 3 decimals
|
||||||
* (~100 m, far below a pin's size at any zoom this map shows).
|
* (~100 m, far below a pin's size at any zoom this map shows).
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const POSTERS = join(OUT, 'posters');
|
||||||
mkdirSync(POSTERS, { recursive: true });
|
mkdirSync(POSTERS, { recursive: true });
|
||||||
|
|
||||||
// ── THE SERIES ──────────────────────────────────────────────────────────────
|
// ── THE SERIES ──────────────────────────────────────────────────────────────
|
||||||
// ★ THIS FILE IS THE HALF NOBODY WAS LOOKING AT. The Driver reported the country posters carrying
|
// ★ THIS FILE IS THE HALF NOBODY WAS LOOKING AT. The country posters were reported carrying
|
||||||
// UPG-100 framing, and the search went to packet-strings.json — but the block telling the reader to
|
// UPG-100 framing, and the search went to packet-strings.json — but the block telling the reader to
|
||||||
// divide the set among THIRTY disciples is not a packet string at all. It is poster furniture, a
|
// divide the set among THIRTY disciples is not a packet string at all. It is poster furniture, a
|
||||||
// hard-coded array in THIS file (see NETWORKS below), so a series parameter applied to the packet
|
// hard-coded array in THIS file (see NETWORKS below), so a series parameter applied to the packet
|
||||||
|
|
@ -83,9 +83,8 @@ const adm0 = readJSON(join(DATA, 'adm0', 'world-adm0.geojson'));
|
||||||
const adm1Ind = readJSON(join(DATA, 'gb-IND-ADM1.geojson'));
|
const adm1Ind = readJSON(join(DATA, 'gb-IND-ADM1.geojson'));
|
||||||
const adm1Chn = readJSON(join(DATA, 'gb-CHN-ADM1.geojson'));
|
const adm1Chn = readJSON(join(DATA, 'gb-CHN-ADM1.geojson'));
|
||||||
|
|
||||||
// CAPITALS + MAJOR CITIES. Driver: "a typical map poster needs details on the map
|
// CAPITALS + MAJOR CITIES. A map poster needs enough detail to read easily, but not
|
||||||
// that is not hard to see, but not so much that it becomes too complicated" —
|
// so much that it becomes complicated: cities and capitals.
|
||||||
// "cities and capitals makes sense".
|
|
||||||
//
|
//
|
||||||
// Natural Earth 1:10m, PUBLIC DOMAIN: no attribution required, no licence notice to
|
// Natural Earth 1:10m, PUBLIC DOMAIN: no attribution required, no licence notice to
|
||||||
// carry onto a printed packet, and vendored so a build is reproducible offline and
|
// carry onto a printed packet, and vendored so a build is reproducible offline and
|
||||||
|
|
@ -93,7 +92,7 @@ const adm1Chn = readJSON(join(DATA, 'gb-CHN-ADM1.geojson'));
|
||||||
// 170 KB / 2,573 (national + admin-1 capitals, plus cities >= 250k) because a poster
|
// 170 KB / 2,573 (national + admin-1 capitals, plus cities >= 250k) because a poster
|
||||||
// cannot legibly carry 7,342 dots and the packet is downloaded.
|
// cannot legibly carry 7,342 dots and the packet is downloaded.
|
||||||
//
|
//
|
||||||
// ⚠ ROADS WERE DELIBERATELY NOT ADDED, though the Driver asked for "main roads".
|
// ⚠ ROADS WERE DELIBERATELY NOT ADDED, though main roads were asked for.
|
||||||
// Natural Earth's 10m roads layer has ZERO features for LAO, MMR, KHM, VNM, THA,
|
// Natural Earth's 10m roads layer has ZERO features for LAO, MMR, KHM, VNM, THA,
|
||||||
// NPL, BTN, TCD, NER, MLI, SOM, AFG, YEM, IDN, IND and CHN — its coverage is
|
// NPL, BTN, TCD, NER, MLI, SOM, AFG, YEM, IDN, IND and CHN — its coverage is
|
||||||
// essentially USA/CAN/MEX. Shipping it would draw a rich road network on a US map
|
// essentially USA/CAN/MEX. Shipping it would draw a rich road network on a US map
|
||||||
|
|
@ -2242,7 +2241,7 @@ async function renderPoster(zone, pins, zoneFC) {
|
||||||
// to run the SAME number of lines in each column — that is what makes three
|
// to run the SAME number of lines in each column — that is what makes three
|
||||||
// columns read as one band instead of three unequal blocks, and it costs a line
|
// columns read as one band instead of three unequal blocks, and it costs a line
|
||||||
// of height, which goes back to the map.
|
// of height, which goes back to the map.
|
||||||
// ★ FROM series.json, NOT FROM HERE. These three blocks were the Driver's second complaint: they
|
// ★ FROM series.json, NOT FROM HERE. These three blocks were the second reported problem: they
|
||||||
// tell the reader to divide the set among THIRTY disciples, which is coherent for ~30 UPG-100
|
// tell the reader to divide the set among THIRTY disciples, which is coherent for ~30 UPG-100
|
||||||
// zones and incoherent for ~170 country posters. The country wording is not a find-and-replace of
|
// zones and incoherent for ~170 country posters. The country wording is not a find-and-replace of
|
||||||
// "thirty" with "a hundred and seventy" — he changed the SHAPE, from "split the set" to "ask
|
// "thirty" with "a hundred and seventy" — he changed the SHAPE, from "split the set" to "ask
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@
|
||||||
// never disagree with its poster about which groups belong to a zone.
|
// never disagree with its poster about which groups belong to a zone.
|
||||||
//
|
//
|
||||||
// ── EVERY STRING IS IN packet-strings.json ──────────────────────────────────
|
// ── EVERY STRING IS IN packet-strings.json ──────────────────────────────────
|
||||||
// Driver: "it all needs to be generative so that I can generate it in multiple
|
// It all has to be generated, so the packet can be generated in every language.
|
||||||
// languages." So no human-readable text is written in this file. LANG=xx picks a
|
// So no human-readable text is written in this file. LANG=xx picks a
|
||||||
// block; missing keys fall back to English individually, so a partial translation
|
// block; missing keys fall back to English individually, so a partial translation
|
||||||
// renders what it has rather than blanking the page.
|
// renders what it has rather than blanking the page.
|
||||||
//
|
//
|
||||||
|
|
@ -46,7 +46,7 @@ const REPO = join(__dirname, '../../..');
|
||||||
// ★ WHY THIS EXISTS. The country posters shipped rendering the UPG-100 packet: its kicker, its
|
// ★ WHY THIS EXISTS. The country posters shipped rendering the UPG-100 packet: its kicker, its
|
||||||
// "1 of {n} UPG-100 lists" line, its narrowing funnel. Not a mislabel — there was NO series
|
// "1 of {n} UPG-100 lists" line, its narrowing funnel. Not a mislabel — there was NO series
|
||||||
// parameter anywhere, so "the packet" and "the UPG-100 packet" were the same thing and nothing
|
// parameter anywhere, so "the packet" and "the UPG-100 packet" were the same thing and nothing
|
||||||
// could disagree. Reported by the Driver, 2026-09-28.
|
// could disagree.
|
||||||
// UNKNOWN OR ABSENT RESOLVES TO upg100, deliberately: every existing run must render byte-identical
|
// UNKNOWN OR ABSENT RESOLVES TO upg100, deliberately: every existing run must render byte-identical
|
||||||
// output with no env var set, or this refactor is a rewrite wearing a parameter's clothes.
|
// output with no env var set, or this refactor is a rewrite wearing a parameter's clothes.
|
||||||
const SERIES_ALL = JSON.parse(readFileSync(join(__dirname, 'series.json'), 'utf8'));
|
const SERIES_ALL = JSON.parse(readFileSync(join(__dirname, 'series.json'), 'utf8'));
|
||||||
|
|
@ -120,8 +120,8 @@ const BASE_STRINGS = JSON.parse(readFileSync(join(__dirname, 'packet-strings.jso
|
||||||
// The renderer gets `strings` as a whole and does its own per-key locale fallback, so an override
|
// The renderer gets `strings` as a whole and does its own per-key locale fallback, so an override
|
||||||
// applied only to this file's t() would render in the pages THIS file writes and not in the pages
|
// applied only to this file's t() would render in the pages THIS file writes and not in the pages
|
||||||
// the RENDERER writes. One series, two different kickers, and nothing would fail.
|
// the RENDERER writes. One series, two different kickers, and nothing would fail.
|
||||||
// Overrides are ENGLISH-ONLY until the Driver signs the wording off (series.json
|
// Overrides are ENGLISH-ONLY until the wording is approved (series.json
|
||||||
// _draft_awaiting_driver), so they are applied to every locale ON PURPOSE: an English country
|
// _draft_pending_decision), so they are applied to every locale ON PURPOSE: an English country
|
||||||
// kicker is visibly awaiting translation, whereas inheriting the translated UPG-100 kicker would
|
// kicker is visibly awaiting translation, whereas inheriting the translated UPG-100 kicker would
|
||||||
// silently print the WRONG SERIES in fluent Russian. Loud beats wrong.
|
// silently print the WRONG SERIES in fluent Russian. Loud beats wrong.
|
||||||
const SERIES_STRINGS = SERIES.strings || {};
|
const SERIES_STRINGS = SERIES.strings || {};
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"The country posters shipped rendering the UPG-100 packet's framing: the 'UNENGAGED PEOPLE",
|
"The country posters shipped rendering the UPG-100 packet's framing: the 'UNENGAGED PEOPLE",
|
||||||
"GROUPS' kicker, 'This packet is 1 of {n} UPG-100 lists', the UPG-100 narrowing funnel, and a",
|
"GROUPS' kicker, 'This packet is 1 of {n} UPG-100 lists', the UPG-100 narrowing funnel, and a",
|
||||||
"below-map block telling the reader to divide the set among thirty disciples. Coherent for ~30",
|
"below-map block telling the reader to divide the set among thirty disciples. Coherent for ~30",
|
||||||
"UPG-100 zones; incoherent for ~170 countries. Reported by the Driver, 2026-09-28.",
|
"UPG-100 zones; incoherent for ~170 countries.",
|
||||||
"There was no series parameter anywhere: packet.mjs and packet-render.js knew only one series,",
|
"There was no series parameter anywhere: packet.mjs and packet-render.js knew only one series,",
|
||||||
"so 'the packet' and 'the UPG-100 packet' were the same thing and nothing could disagree.",
|
"so 'the packet' and 'the UPG-100 packet' were the same thing and nothing could disagree.",
|
||||||
"",
|
"",
|
||||||
|
|
@ -47,9 +47,9 @@
|
||||||
"end; `strings` below is the half that carries 11 locales."
|
"end; `strings` below is the half that carries 11 locales."
|
||||||
],
|
],
|
||||||
"translation_status": [
|
"translation_status": [
|
||||||
"The `country` English below is a DRAFT taken from the Driver's dictated shape and is NOT signed",
|
"The `country` English below is a DRAFT, provisional, pending a product decision -- see",
|
||||||
"off -- see _draft_awaiting_driver on that entry. NO translations are to be made from it: a",
|
"_draft_pending_decision on that entry. NO translations are to be made from it: a",
|
||||||
"paraphrase multiplied by 11 locales is 11 wrong strings. When he signs off the English, the",
|
"paraphrase multiplied by 11 locales is 11 wrong strings. Once the English is approved, the",
|
||||||
"strings go to a HUMAN translator via dx-shepherd, which is this file's established practice --",
|
"strings go to a HUMAN translator via dx-shepherd, which is this file's established practice --",
|
||||||
"all 11 locales of packet-strings.json carry real translations, never English echoes."
|
"all 11 locales of packet-strings.json carry real translations, never English echoes."
|
||||||
]
|
]
|
||||||
|
|
@ -96,11 +96,10 @@
|
||||||
"'Series: List of 100 UPG's · map 11 of 28' and a side box calling it 'one of a 30-map set",
|
"'Series: List of 100 UPG's · map 11 of 28' and a side box calling it 'one of a 30-map set",
|
||||||
"called the List of 100 UPG's'. My check:series-isolation passed because its vocabulary was a",
|
"called the List of 100 UPG's'. My check:series-isolation passed because its vocabulary was a",
|
||||||
"HAND LIST of the phrases I had already found -- a PASS from it was only ever a lower bound.",
|
"HAND LIST of the phrases I had already found -- a PASS from it was only ever a lower bound.",
|
||||||
"seriesName 'Country Maps' is the Driver's own wording, recorded in a generate.mjs design note",
|
"seriesName 'Country Maps' is the series' agreed name, recorded in a generate.mjs design note",
|
||||||
"dated 2026-08-01: 'we'll call this series the Country Map series... inside of the quotes say",
|
"dated 2026-08-01, which also made the series name a parameterized field. The kicker says UNREACHED rather",
|
||||||
"Country Maps. So that series should be a parameterized field.' The kicker says UNREACHED rather",
|
|
||||||
"than UNENGAGED because a country sheet lists the unreached groups living there, which is a",
|
"than UNENGAGED because a country sheet lists the unreached groups living there, which is a",
|
||||||
"wider set than the unengaged -- see _draft_awaiting_driver; this wording is part of that draft."
|
"wider set than the unengaged -- see _draft_pending_decision; this wording is part of that draft."
|
||||||
],
|
],
|
||||||
"_funnel_why": [
|
"_funnel_why": [
|
||||||
"The funnel narrows every people group on earth down to THIS UPG-100 list (Project 2000/7000/",
|
"The funnel narrows every people group on earth down to THIS UPG-100 list (Project 2000/7000/",
|
||||||
|
|
@ -109,10 +108,9 @@
|
||||||
"Dropping it also removes 12 of the 17 series-specific keys from the translation bill: a page",
|
"Dropping it also removes 12 of the 17 series-specific keys from the translation bill: a page",
|
||||||
"that does not render needs no locales."
|
"that does not render needs no locales."
|
||||||
],
|
],
|
||||||
"_draft_awaiting_driver": [
|
"_draft_pending_decision": [
|
||||||
"THE ENGLISH BELOW IS A DRAFT, NOT APPROVED. It follows the shape the Driver dictated on",
|
"THE ENGLISH BELOW IS A DRAFT, NOT APPROVED: provisional, pending a product decision on how",
|
||||||
"2026-09-28 but he has not signed off the wording. regions-plugin has asked him to dictate the",
|
"the three blocks should print. Do not translate, print at scale, or quote as final",
|
||||||
"three blocks as he wants them printed. Do not translate, print at scale, or quote as final",
|
|
||||||
"until that lands; replace this key with the approved text when it does."
|
"until that lands; replace this key with the approved text when it does."
|
||||||
],
|
],
|
||||||
"strings": {
|
"strings": {
|
||||||
|
|
@ -142,7 +140,7 @@
|
||||||
"_networks_why": [
|
"_networks_why": [
|
||||||
"NOT a find-and-replace of thirty with a hundred and seventy. 'Divide the set among thirty",
|
"NOT a find-and-replace of thirty with a hundred and seventy. 'Divide the set among thirty",
|
||||||
"disciples' is coherent at 30 and incoherent at ~170 (172 countries + China + India, per",
|
"disciples' is coherent at 30 and incoherent at ~170 (172 countries + China + India, per",
|
||||||
"emit-countries.mjs), so the Driver replaced the SHAPE: not 'split the set' but 'ask which one,",
|
"emit-countries.mjs), so the SHAPE was replaced: not 'split the set' but 'ask which one,",
|
||||||
"then give them that poster'. The numbers are gone from the headings for the same reason."
|
"then give them that poster'. The numbers are gone from the headings for the same reason."
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1340,7 +1340,7 @@ const ROUTE = {
|
||||||
for (const b of built) {
|
for (const b of built) {
|
||||||
const app = ROUTE[b.name];
|
const app = ROUTE[b.name];
|
||||||
if (!app) continue;
|
if (!app) continue;
|
||||||
// ★ THE LEAK THIS LOOP CAUSED (found live 2026-09-24, Driver-critical): emit() already
|
// ★ THE LEAK THIS LOOP CAUSED (found live 2026-09-24, critical): emit() already
|
||||||
// respects PRIVATE_TABLES and keeps those out of the repo -- but THIS loop copied from
|
// respects PRIVATE_TABLES and keeps those out of the repo -- but THIS loop copied from
|
||||||
// out/tables into the BUNDLE's data dir without the same check, so field-access-visas.json
|
// out/tables into the BUNDLE's data dir without the same check, so field-access-visas.json
|
||||||
// landed in plugin-designs/key-cities/data/tables/, flowed into the public snapshot AND the
|
// landed in plugin-designs/key-cities/data/tables/, flowed into the public snapshot AND the
|
||||||
|
|
|
||||||
|
|
@ -100,8 +100,8 @@ export const STYLE_REGISTRY = [
|
||||||
bestFor: ['research-dashboard'] },
|
bestFor: ['research-dashboard'] },
|
||||||
|
|
||||||
// ── GALAXY — black ground, BLUE water ────────────────────────────────────────────────────
|
// ── GALAXY — black ground, BLUE water ────────────────────────────────────────────────────
|
||||||
// DRIVER 2026-09-23: "the map style needs to have the BLUE WATER with like the BLACK GALAXY
|
// The ask (2026-09-23): BLUE WATER against a BLACK GALAXY background. The continents go to deep
|
||||||
// background". The continents go to deep space; the oceans stay unmistakably water.
|
// space; the oceans stay unmistakably water.
|
||||||
//
|
//
|
||||||
// ★ WHY THIS IS A BASE STYLE + A PAINT PATCH, NOT A HAND-AUTHORED style.json. Writing a whole
|
// ★ WHY THIS IS A BASE STYLE + A PAINT PATCH, NOT A HAND-AUTHORED style.json. Writing a whole
|
||||||
// style document would mean owning ~47 layers, a glyph endpoint and a sprite URL forever, and
|
// style document would mean owning ~47 layers, a glyph endpoint and a sprite URL forever, and
|
||||||
|
|
@ -121,10 +121,9 @@ export const STYLE_REGISTRY = [
|
||||||
// non-text threshold, so the coastline is a real edge rather than a tonal hint. Darker,
|
// non-text threshold, so the coastline is a real edge rather than a tonal hint. Darker,
|
||||||
// moodier blues were tried and measured first: #1b3a6b 1.74:1 and #1e4d8f 2.34:1 both fall
|
// moodier blues were tried and measured first: #1b3a6b 1.74:1 and #1e4d8f 2.34:1 both fall
|
||||||
// UNDER 3:1, which is how an ocean ends up merely looking like slightly-less-black land.
|
// UNDER 3:1, which is how an ocean ends up merely looking like slightly-less-black land.
|
||||||
/* ★ CORRECTED 2026-09-23 — THE FIRST GALAXY WAS A MISREADING OF THE ASK, and the Driver said so:
|
/* ★ CORRECTED 2026-09-23 — THE FIRST GALAXY WAS A MISREADING OF THE ASK. Not a dark basemap: a
|
||||||
"I don't want a dark basemap, I didn't ask for one. I asked for a spherical GLOBE with a black
|
spherical GLOBE on a black background (the galaxy, as the Mapbox map has it), with BLUE OCEANS
|
||||||
background — the galaxy, like the Mapbox map — with BLUE OCEANS like the Mapbox map, and the
|
like the Mapbox map, and land that is NOT black.
|
||||||
land was NOT to be black."
|
|
||||||
The first attempt tinted a dark basemap (black land, recoloured water) — the night side of
|
The first attempt tinted a dark basemap (black land, recoloured water) — the night side of
|
||||||
Earth. What he described is the DAY side seen from space: NORMAL land colours, the default
|
Earth. What he described is the DAY side seen from space: NORMAL land colours, the default
|
||||||
blue oceans, and the blackness belongs to the SPACE BEHIND THE SPHERE, not to the ground.
|
blue oceans, and the blackness belongs to the SPACE BEHIND THE SPHERE, not to the ground.
|
||||||
|
|
@ -133,7 +132,7 @@ export const STYLE_REGISTRY = [
|
||||||
background showing through outside the sphere (see DashboardMapPanel's .rd-map rule). */
|
background showing through outside the sphere (see DashboardMapPanel's .rd-map rule). */
|
||||||
{ id: 'galaxy', label: 'Galaxy', mood: 'the day side of Earth floating in space — normal land, blue oceans, black void behind the sphere',
|
{ id: 'galaxy', label: 'Galaxy', mood: 'the day side of Earth floating in space — normal land, blue oceans, black void behind the sphere',
|
||||||
mapbox: 'mapbox://styles/mapbox/standard',
|
mapbox: 'mapbox://styles/mapbox/standard',
|
||||||
// ★ FOUND, NOT AUTHORED (Driver: "I want it replaced with something you can find online").
|
// ★ FOUND, NOT AUTHORED: replaced with a style that already exists online.
|
||||||
// VersaTiles Colorful — a maintained, production-free, keyless style (280 layers) whose ocean
|
// VersaTiles Colorful — a maintained, production-free, keyless style (280 layers) whose ocean
|
||||||
// is a real blue (#bfd9f2), the closest of the surveyed free styles to the Mapbox look. The
|
// is a real blue (#bfd9f2), the closest of the surveyed free styles to the Mapbox look. The
|
||||||
// alternatives measured and rejected: CARTO Voyager's water is pale teal #b0d0d6; OSM
|
// alternatives measured and rejected: CARTO Voyager's water is pale teal #b0d0d6; OSM
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* formatCount — the ONE place the fleet decides how a COUNT is punctuated.
|
* formatCount — the ONE place the fleet decides how a COUNT is punctuated.
|
||||||
*
|
*
|
||||||
* ★ IT HOLDS en-US ON PURPOSE. Whether numerals follow the reader's locale is a DRIVER decision and
|
* ★ IT HOLDS en-US ON PURPOSE. Whether numerals follow the reader's locale is provisional, pending a
|
||||||
* it is currently held, so this file changes nothing about what anyone sees: every caller renders
|
* product decision, so this file changes nothing about what anyone sees: every caller renders
|
||||||
* exactly the digits and separators it rendered before. What it changes is the number of places that
|
* exactly the digits and separators it rendered before. What it changes is the number of places that
|
||||||
* decision is written down — from thirteen call sites across four bundles to one.
|
* decision is written down — from thirteen call sites across four bundles to one.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
2
library/vendor/cldr/README.md
vendored
2
library/vendor/cldr/README.md
vendored
|
|
@ -31,7 +31,7 @@ fails, so a bad refresh cannot land quietly.
|
||||||
|
|
||||||
`library/scripts/research/out/iso3-to-iso2.json` is a DIFFERENT map with a different job: it is the
|
`library/scripts/research/out/iso3-to-iso2.json` is a DIFFERENT map with a different job: it is the
|
||||||
176-country research spine that drives the data fetches, and expanding it changes what gets fetched —
|
176-country research spine that drives the data fetches, and expanding it changes what gets fetched —
|
||||||
a data-scope decision that belongs to the Driver, not a side effect of adding country names. The map
|
a product decision about data scope, not a side effect of adding country names. The map
|
||||||
derived here is for DISPLAY NAMES ONLY and drives no fetch. CLDR agrees with all 176 values in the
|
derived here is for DISPLAY NAMES ONLY and drives no fetch. CLDR agrees with all 176 values in the
|
||||||
research map, which is a useful independent check on it, but the two files are not interchangeable.
|
research map, which is a useful independent check on it, but the two files are not interchangeable.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue