Merge main into bundler-2/work
This commit is contained in:
commit
be77ea954c
69 changed files with 1455 additions and 530 deletions
|
|
@ -22,6 +22,8 @@ and a change here can break other maps.
|
|||
`import … from '@map/components/ResearchMapSideMenu.vue'`
|
||||
- **SideMenuDrawer** — SideMenuDrawer.vue — Slide-in side panel for the map application.
|
||||
`import … from '@map/components/SideMenuDrawer.vue'`
|
||||
- **ViewSelect** — ViewSelect.vue — the "SELECT VIEW" switcher: a fieldset + legend wrapping a row of
|
||||
`import … from '@map/components/ViewSelect.vue'`
|
||||
- **BarChart** — BarChart.vue — THE SHARED HORIZONTAL BAR CHART (hand-rolled SVG, zero dependencies).
|
||||
`import … from '@map/components/charts/BarChart.vue'`
|
||||
- **PinHint** — PinHint.vue — the conversion coach-mark (bottom-center of any map).
|
||||
|
|
|
|||
|
|
@ -62,7 +62,16 @@ useShadowStyles(
|
|||
'.seg button{ position:relative; display:flex; align-items:center; gap:6px;' +
|
||||
' border:0; background:none; color:var(--ui-text-muted,#6d766e);' +
|
||||
' padding:6px 12px 7px; font-size:13px; font-weight:500; cursor:pointer;' +
|
||||
' transition:color .15s; font-family:inherit; }' +
|
||||
' transition:color .15s; font-family:inherit;' +
|
||||
// ONE LINE PER TAB. Ported from the shell's own mobile rule (app-wrapper-material
|
||||
// index.html, max-width:960px: white-space:nowrap + ellipsis) — the extraction that
|
||||
// created this component copied the DESKTOP metrics verbatim but left that hardening
|
||||
// behind, so the leaf and the shell disagreed on narrow viewports.
|
||||
// MEASURED at 390px with the label "Prayer Resources": the fieldset is width-capped
|
||||
// by the header, the TEXT wrapped inside the button (28 -> 43px) and the strip grew
|
||||
// 45 -> 60px, while the shell held 45px. The row of tabs never wrapped; only the
|
||||
// words inside one did. Ellipsis is the better failure than a taller header.
|
||||
' white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }' +
|
||||
'.seg button::after{ content:""; position:absolute; left:8px; right:8px; bottom:0;' +
|
||||
' height:2px; border-radius:2px 2px 0 0; background:transparent;' +
|
||||
' transition:background .15s; }' +
|
||||
|
|
|
|||
|
|
@ -148,7 +148,24 @@ function positionMenu() {
|
|||
// no edge-sharing placement is reachable — fall back to the hard clamp, since an
|
||||
// unreachable menu is worse than a detached one. That last case cannot mislead: the
|
||||
// trigger is not on screen to look detached FROM.
|
||||
const width = Math.round(Math.min(r.width, vw - EDGE * 2))
|
||||
// ── ★ 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
|
||||
// as you want it to be… so you can fit MORE OF THEM ON ONE ROW WITHOUT WORD WRAPPING."
|
||||
//
|
||||
// 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:
|
||||
// - the CLOSED TRIGGER competes for a shared row, so every px it takes is a px another axis
|
||||
// cannot have; it should be as small as its label allows.
|
||||
// - the OPEN MENU is transient and overlays everything (position:fixed, z-index above the
|
||||
// world), so its width costs the layout NOTHING — no sibling reflows around it.
|
||||
// Sizing them together meant narrowing the trigger also narrowed the menu, truncating the option
|
||||
// labels that are the whole reason a narrow trigger is acceptable. So they are separated here.
|
||||
//
|
||||
// The menu is therefore max(trigger, MENU_MIN) and still capped to the viewport. It is a FLOOR,
|
||||
// not a fixed size: a trigger WIDER than the floor (a consumer that raised --dx-filter-w) keeps
|
||||
// the old edge-matched look, so this cannot regress a host that was already happy.
|
||||
const MENU_MIN = 240
|
||||
const width = Math.round(Math.min(Math.max(r.width, MENU_MIN), vw - EDGE * 2))
|
||||
const triggerOnScreen = r.right > EDGE && r.left < vw - EDGE
|
||||
let left
|
||||
if (!triggerOnScreen) {
|
||||
|
|
@ -361,16 +378,49 @@ onBeforeUnmount(() => {
|
|||
--dx-filter-w is a TOKEN, not a literal, for the same reason every colour here is: this
|
||||
component is shared by people-groups-grid, big-picture AND the research-dashboard, so a
|
||||
consumer that needs a different trigger size sets the token at :host instead of forking the
|
||||
component. 168px is sized to the Driver's brief — "just big enough to express the first or
|
||||
second word" — measured against the real labels: it shows "Region: North Am…", i.e. the axis
|
||||
name plus the first word or two, which is what makes the row scannable without reading it. */
|
||||
component. 168px was sized to an earlier brief — "just big enough to express the first or
|
||||
second word".
|
||||
|
||||
── ★ 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
|
||||
wide. Make the filters very NARROW… so you can fit MORE OF THEM ON ONE ROW WITHOUT WORD WRAPPING.
|
||||
And that way we can decrease the thickness of the headers."
|
||||
|
||||
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 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.
|
||||
|
||||
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:
|
||||
2px border + 16px horizontal padding (2 x --dx-filter-pad-x, trimmed 12 -> 8 with this card) +
|
||||
6px gap + 12px chevron = 36px. So a 128px trigger leaves 92px for text.
|
||||
|
||||
Every axis label rendered at 0.875rem/system-ui and measured live:
|
||||
Bloc 29.8 · Status 44.6 · Prayer 45.2 · People 46.5 · Region 47.9 · Country 55.6 ·
|
||||
Religion 55.7 · Language 69.1 · Engagement 89.3
|
||||
116px was chosen first, on the reasoning that "Language" (69.1) was the longest of the SEVEN
|
||||
CURRENTLY SHIPPED and fit its 80px budget with room to spare. That was true and still wrong:
|
||||
this component is SHARED, and this file's own history records an eighth "engagement/Status" axis
|
||||
that has been present before. At 89.3px "Engagement" overflowed the 116px budget by 9.3px — the
|
||||
axis NAME would have ellipsised, which is the one truncation that must never happen (a trigger
|
||||
reading "Engageme…" is unscannable, and unlike a truncated VALUE it has no menu to recover it).
|
||||
128px fits the longest KNOWN label with 2.7px to spare and is still a 24% cut from 168px.
|
||||
|
||||
The floor is a TYPE metric: it moves only if the labels or the font do — and the font is not
|
||||
allowed to move (below). If an axis longer than "Engagement" is ever added, re-measure rather
|
||||
than letting it truncate.
|
||||
|
||||
★ THE FONT-SIZE IS NOT A LEVER. The Driver has rejected shrinking it twice ("super tiny text
|
||||
that cannot be read"). 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. */
|
||||
.filter-dropdown { position: relative; flex: 0 0 auto;
|
||||
width: var(--dx-filter-w, 168px); min-width: var(--dx-filter-w, 168px); max-width: var(--dx-filter-w, 168px); }
|
||||
width: var(--dx-filter-w, 128px); min-width: var(--dx-filter-w, 128px); max-width: var(--dx-filter-w, 128px); }
|
||||
/* Mobile keeps the SAME fixed-box contract — only the number shrinks. The old rule here was
|
||||
`flex:1 1 100%`, which full-widthed the trigger and wrapped the row; that is the growth bug in
|
||||
its most extreme form, so it cannot come back as a "mobile exception". */
|
||||
@media (max-width: 576px) { .filter-dropdown {
|
||||
width: var(--dx-filter-w-sm, 140px); min-width: var(--dx-filter-w-sm, 140px); max-width: var(--dx-filter-w-sm, 140px); } }
|
||||
width: var(--dx-filter-w-sm, 108px); min-width: var(--dx-filter-w-sm, 108px); max-width: var(--dx-filter-w-sm, 108px); } }
|
||||
/* ★ THE TRIGGER'S HEIGHT IS THE FILTER ROW'S HEIGHT, and it was the last unattributed term
|
||||
(card BUILD-SELECTED-ROW-MUCH-THINNER-AND-CENTRED — "the filter row is way too thick").
|
||||
MEASURED at 1600x900 standalone, the filters band's 67px decomposes with nothing left over:
|
||||
|
|
@ -382,14 +432,17 @@ onBeforeUnmount(() => {
|
|||
padding 8->5, the selections row 24->18). The trigger was never touched, and at 30px it is the
|
||||
single largest contributor — which is why the band kept measuring ~67 no matter how much was
|
||||
shaved off the others. Vertical padding 6 -> 4 takes the trigger to 26 and the band to 63.
|
||||
★ VERTICAL ONLY. The horizontal 12px is UNCHANGED and must stay: `check:filter-width` asserts
|
||||
every trigger is exactly --dx-filter-w (168px) before and after selecting the longest option,
|
||||
and the label's ellipsis budget is that width minus this padding and the chevron. Touching the
|
||||
horizontal value would move a gate that exists to keep the centred stack from re-flowing.
|
||||
Tokenized so a consumer can trade density back without forking the component, matching how
|
||||
--dx-filter-w already handles the width. */
|
||||
★ THE HORIZONTAL PADDING IS NOW A LEVER TOO — 12px -> 8px, and the earlier "must stay" note it
|
||||
replaces was over-stated (card BUILD-SLIM-FILTERS-NARROW-TRIGGER-WIDE-MENU). That note said
|
||||
touching it "would move a gate", having read check:filter-width as asserting a literal 168px.
|
||||
It does not: the gate records every trigger's width with nothing selected and requires the SAME
|
||||
widths after selecting the longest option — it asserts STABILITY, not a number. A uniform
|
||||
narrowing moves both arrays together and the gate stays green (verified by running it).
|
||||
The 8px is spent where it is worth most: it is 8px of the 72px text budget handed back to the
|
||||
axis label, which is what lets the trigger reach 116px without clipping "Language".
|
||||
Tokenized for the same reason as the width — a consumer trades density back without forking. */
|
||||
.filter-dropdown__trigger {
|
||||
width: 100%; padding: var(--dx-filter-pad-y, 4px) 12px; cursor: pointer; display: flex; justify-content: space-between;
|
||||
width: 100%; padding: var(--dx-filter-pad-y, 4px) var(--dx-filter-pad-x, 8px); cursor: pointer; display: flex; justify-content: space-between;
|
||||
/* Pairs its own foreground with its own background for the same reason as the menu:
|
||||
`color: inherit` over a light background rendered near-white-on-white inside the
|
||||
cap-4 dark header. Background and text are set together, never half-inherited. */
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ A zero-dependency Fuse.js-shaped matcher (so no npm dep is added to the shared
|
|||
```js
|
||||
const fuse = new Fuse(list, {
|
||||
keys: ['name', 'country_code.label', 'religion.label'], // dotted paths
|
||||
threshold: 0, // 0 = exact substring only; >0 = fuzzy subsequence
|
||||
threshold: 0, // 0 = exact substring only; >0 = fuzzy (k-errors edit distance, Fuse-parity)
|
||||
minMatchCharLength: 3, // shortest run counted as a match
|
||||
})
|
||||
fuse.search(term) // -> [{ item, matches: [{ key, indices: [[start,end], …] }] }]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
* with { keys, threshold, includeMatches, ignoreLocation, minMatchCharLength }. We reproduce
|
||||
* exactly the parts of that contract the grid relies on:
|
||||
* - keys: array of dotted paths ('name', 'country_code.label', …) read from each item
|
||||
* - threshold: 0 = exact substring only; >0 = fuzzy (subsequence) allowed
|
||||
* - threshold: 0 = exact substring only; >0 = fuzzy (k-errors edit distance, Fuse-parity)
|
||||
* - minMatchCharLength: shortest run counted as a match
|
||||
* - .search(term) -> [{ item, matches:[{ key, indices:[[start,end], …] }] }]
|
||||
* The `matches[].indices` drive the <span class="search-highlight"> wrapping in the grid,
|
||||
|
|
@ -43,29 +43,57 @@ function exactIndices(haystack, needle) {
|
|||
return out
|
||||
}
|
||||
|
||||
// Fuzzy (subsequence) match: every char of `needle` appears in order in `haystack`.
|
||||
// Returns the runs of consecutive matched positions, or null if not all chars matched.
|
||||
function fuzzyIndices(haystack, needle) {
|
||||
// ── Fuzzy match, FUSE-PARITY (2026-09-23) ────────────────────────────────────────────────
|
||||
// This used to be a strict SUBSEQUENCE matcher ("every char of needle appears in order"), and
|
||||
// that is NOT what Fuse.js does. The difference bit in production: DX-life's real Fuse
|
||||
// (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 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.
|
||||
//
|
||||
// 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),
|
||||
// matching iff errors/needle.length <= threshold -- the same score contract Fuse applies with
|
||||
// ignoreLocation:true. Highlight indices are the best-scoring window's char span.
|
||||
function approxIndices(haystack, needle, threshold) {
|
||||
const h = haystack.toLowerCase()
|
||||
const n = needle.toLowerCase()
|
||||
const runs = []
|
||||
let hi = 0
|
||||
let runStart = -1
|
||||
let runEnd = -1
|
||||
for (let ni = 0; ni < n.length; ni++) {
|
||||
const ch = n[ni]
|
||||
let found = -1
|
||||
for (let k = hi; k < h.length; k++) {
|
||||
if (h[k] === ch) { found = k; break }
|
||||
const m = n.length
|
||||
if (!m || !h.length) return null
|
||||
const maxErr = Math.floor(threshold * m)
|
||||
if (maxErr <= 0) return null
|
||||
// dp[j] = min errors matching the whole prefix n[0..i] ending at text position j.
|
||||
// Row 0 is all zeros: the match may START anywhere (that is the "search" variant).
|
||||
let prev = new Array(h.length + 1).fill(0)
|
||||
let cur = new Array(h.length + 1)
|
||||
for (let i = 1; i <= m; i++) {
|
||||
cur[0] = i
|
||||
for (let j = 1; j <= h.length; j++) {
|
||||
const sub = prev[j - 1] + (n[i - 1] === h[j - 1] ? 0 : 1)
|
||||
cur[j] = Math.min(sub, prev[j] + 1, cur[j - 1] + 1)
|
||||
}
|
||||
if (found === -1) return null
|
||||
if (runStart === -1) { runStart = runEnd = found }
|
||||
else if (found === runEnd + 1) { runEnd = found }
|
||||
else { runs.push([runStart, runEnd]); runStart = runEnd = found }
|
||||
hi = found + 1
|
||||
;[prev, cur] = [cur, prev]
|
||||
}
|
||||
if (runStart !== -1) runs.push([runStart, runEnd])
|
||||
return runs
|
||||
let best = Infinity, bestEnd = -1
|
||||
for (let j = 1; j <= h.length; j++) if (prev[j] < best) { best = prev[j]; bestEnd = j }
|
||||
if (best > maxErr) return null
|
||||
const start = Math.max(0, bestEnd - m)
|
||||
return [[start, bestEnd - 1]]
|
||||
}
|
||||
|
||||
/**
|
||||
* Low-level predicate shared with the dashboard's search matcher (useDashboardShell.js):
|
||||
* does `text` match `term` under the DX-life Fuse contract (exact substring first, then
|
||||
* k-errors fuzzy when term.length >= minMatchCharLength)? ONE matcher, every search surface.
|
||||
*/
|
||||
export function fuzzyMatches(text, term, { threshold = 0.4, minMatchCharLength = 3 } = {}) {
|
||||
const raw = String(text ?? '')
|
||||
const q = String(term ?? '').trim()
|
||||
if (!q) return true
|
||||
if (!raw) return false
|
||||
if (raw.toLowerCase().includes(q.toLowerCase())) return true
|
||||
if (threshold === 0 || q.length < minMatchCharLength) return false
|
||||
return approxIndices(raw, q, threshold) !== null
|
||||
}
|
||||
|
||||
export default class Fuse {
|
||||
|
|
@ -89,12 +117,8 @@ export default class Fuse {
|
|||
if (typeof raw !== 'string' || !raw) continue
|
||||
let indices = exactIndices(raw, q)
|
||||
if (indices.length === 0 && !exactOnly) {
|
||||
const fz = fuzzyIndices(raw, q)
|
||||
if (fz) {
|
||||
// keep only runs at least minMatchCharLength long (mirrors Fuse's minMatch)
|
||||
const kept = fz.filter(([s, e]) => e - s + 1 >= Math.min(this.minMatchCharLength, q.length))
|
||||
if (kept.length) indices = kept
|
||||
}
|
||||
const fz = approxIndices(raw, q, this.threshold)
|
||||
if (fz) indices = fz
|
||||
}
|
||||
if (indices.length) matches.push({ key, indices })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const OPENFREEMAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty'
|
|||
|
||||
// Named styles: the 12-style pack. A profile-config style of 'ink' /
|
||||
// 'midnight-glow' / … resolves per engine here; raw URLs pass through.
|
||||
import { resolveNamedStyle } from '../styles/registry.js'
|
||||
import { resolveNamedStyle, applyStylePaint } from '../styles/registry.js'
|
||||
|
||||
/** The live engine global for a provider, or undefined if not yet loaded. */
|
||||
function engineGlobal(provider) {
|
||||
|
|
@ -238,6 +238,12 @@ export function useMapInstance({
|
|||
// A pinned value is therefore never lowered, only raised. Leave it undefined to
|
||||
// get the pure dynamic floor.
|
||||
minZoom = undefined,
|
||||
// ★ skipWorldFloor: a GLOBE host opts out of the desktop no-duplicate-world minZoom floor.
|
||||
// That floor exists to stop a wide mercator viewport showing the world twice; on the globe
|
||||
// projection there is no second world -- the planet is a sphere -- so the floor only steals
|
||||
// zoom-out range (measured: it pinned the dashboard at ~1.45 when the whole point of a globe
|
||||
// is pulling back to see it whole). The pinned minZoom (or the engine floor) still applies.
|
||||
skipWorldFloor = false,
|
||||
maxZoom = 18,
|
||||
pitch = 0,
|
||||
bearing = 0,
|
||||
|
|
@ -351,6 +357,7 @@ export function useMapInstance({
|
|||
// up automatically on an orientation/breakpoint flip into mobile.
|
||||
const isDesktop = () => typeof window !== 'undefined' && window.innerWidth >= 1024
|
||||
const floorFor = (W, H) => {
|
||||
if (skipWorldFloor) return (typeof minZoom === 'number') ? minZoom : MIN_ZOOM_FLOOR
|
||||
if (!isDesktop()) return MIN_ZOOM_FLOOR // mobile/tablet — no floor
|
||||
const f = computeWorldMinZoom(W, H, maxZoom, southCutoff)
|
||||
return (typeof minZoom === 'number') ? Math.max(minZoom, f) : f
|
||||
|
|
@ -547,6 +554,19 @@ export function useMapInstance({
|
|||
}
|
||||
|
||||
map.value.on('style.load', () => {
|
||||
// ── NAMED-STYLE PAINT PATCH (e.g. 'galaxy': blue water on a black ground) ──────────────
|
||||
// style.load is the ONLY correct moment: setPaintProperty before the style document has
|
||||
// parsed throws, and re-applying here also survives a later setStyle() — the event fires
|
||||
// again, so a style swap cannot silently drop the patch.
|
||||
// Nothing happens for styles with no `paint` in the registry, so every existing map is
|
||||
// byte-for-byte unchanged.
|
||||
if (_named.paint && map.value) {
|
||||
const hit = applyStylePaint(map.value, _named.paint)
|
||||
// Log what the LOADED style actually had — layer ids differ between Mapbox and MapLibre
|
||||
// and have moved across Mapbox style versions, so this is the only trustworthy record
|
||||
// of which ids are real. A patch that matched nothing is a silent no-op otherwise.
|
||||
console.info('[useMapInstance] style paint applied (' + provider + '):', hit.join(', ') || 'NONE MATCHED')
|
||||
}
|
||||
// Resize after style.load in a rAF: container layout may not have settled yet,
|
||||
// especially when the panel was hidden (display:none) at startup.
|
||||
requestAnimationFrame(() => {
|
||||
|
|
|
|||
|
|
@ -380,7 +380,17 @@ const VTL_SHADOW_CSS = `
|
|||
one viewport. The clipping now belongs to .vtl-host, which owns a real scrollbar, so the content
|
||||
is both contained AND reachable. Dropping paint costs the collapse nothing: paint never
|
||||
contributed reflow isolation, only clipping. */
|
||||
.vtl-content { flex:1 1 auto; position:relative; min-height:0; contain:layout; }
|
||||
.vtl-content { flex:1 1 auto; position:relative; min-height:0; }
|
||||
/* ★ NO containment AT ALL on .vtl-content (BUG-FULLSCREEN-MODALS-UNDERSIZED-INSIDE-THE-CAP5-SHELL,
|
||||
measured 2026-09-23). contain:paint was dropped earlier for the scroll bug; contain:layout
|
||||
stayed -- but LAYOUT containment ALSO makes this element the containing block for every
|
||||
position:fixed descendant (CSS Containment spec), so all six modal scrims hosted in the shell
|
||||
(PosterDialog, DataTable .dt-scrim, LegendTools .lt-overlay, SemanticTreeLegend export
|
||||
backdrop, both poster-wall .modal-scrim) resolved inset:0 to the CONTENT PANE: measured, a
|
||||
fixed inset:0 probe here read x=256 w=1184 on a 1440 viewport, leaving the 256px nav rail
|
||||
uncovered and CLICKABLE behind an open modal. Visibly almost right, interactively wrong.
|
||||
The perf rationale it was kept for does not hold: when the nav collapses, this element's own
|
||||
width changes, and a width change re-lays-out the subtree with or without containment. */
|
||||
/* ★ THE PANE OWNS THE SCROLL (BUG-STILL-CANNOT-SCROLL-DOWN-TO-THE-GRID). Measured in the cap-5
|
||||
shell: the dashboard-plus-grid composite mounted here reported scrollHeight 3380 / clientHeight
|
||||
769 — 2611px of real, mounted, populated content (the people-groups grid, h=2586) that NO element
|
||||
|
|
@ -813,7 +823,17 @@ export default defineComponent({
|
|||
one viewport. The clipping now belongs to .vtl-host, which owns a real scrollbar, so the content
|
||||
is both contained AND reachable. Dropping paint costs the collapse nothing: paint never
|
||||
contributed reflow isolation, only clipping. */
|
||||
.vtl-content { flex:1 1 auto; position:relative; min-height:0; contain:layout; }
|
||||
.vtl-content { flex:1 1 auto; position:relative; min-height:0; }
|
||||
/* ★ NO containment AT ALL on .vtl-content (BUG-FULLSCREEN-MODALS-UNDERSIZED-INSIDE-THE-CAP5-SHELL,
|
||||
measured 2026-09-23). contain:paint was dropped earlier for the scroll bug; contain:layout
|
||||
stayed -- but LAYOUT containment ALSO makes this element the containing block for every
|
||||
position:fixed descendant (CSS Containment spec), so all six modal scrims hosted in the shell
|
||||
(PosterDialog, DataTable .dt-scrim, LegendTools .lt-overlay, SemanticTreeLegend export
|
||||
backdrop, both poster-wall .modal-scrim) resolved inset:0 to the CONTENT PANE: measured, a
|
||||
fixed inset:0 probe here read x=256 w=1184 on a 1440 viewport, leaving the 256px nav rail
|
||||
uncovered and CLICKABLE behind an open modal. Visibly almost right, interactively wrong.
|
||||
The perf rationale it was kept for does not hold: when the nav collapses, this element's own
|
||||
width changes, and a width change re-lays-out the subtree with or without containment. */
|
||||
/* ★ THE PANE OWNS THE SCROLL (BUG-STILL-CANNOT-SCROLL-DOWN-TO-THE-GRID). Measured in the cap-5
|
||||
shell: the dashboard-plus-grid composite mounted here reported scrollHeight 3380 / clientHeight
|
||||
769 — 2611px of real, mounted, populated content (the people-groups grid, h=2586) that NO element
|
||||
|
|
|
|||
|
|
@ -631,6 +631,11 @@ const VIEWS = [
|
|||
{ id:'tree', label:'Tree', icon:'tree' },
|
||||
]
|
||||
let view = 'map'
|
||||
// Which profile the tab strip was last drawn FOR, so drawSeg can tell "same profile,
|
||||
// user picked a tab" from "profile changed, reset to its first view". Declared with
|
||||
// `var`-like hoisting in mind: it is read inside drawSeg, which only ever runs after
|
||||
// this module body has executed, so a `let` here is safe (cf. DEFAULT_PROFILE below).
|
||||
let segProfile = null
|
||||
// The tabs are PER-PROFILE, not a fixed app-wide set. A profile that declares
|
||||
// `panes` shows exactly those and no others — upg-100 has a map and a poster wall,
|
||||
// so it shows two tabs, not three with a dead one. A profile with no `panes` key
|
||||
|
|
@ -666,7 +671,15 @@ function drawSeg(){
|
|||
if (_sel) _sel.style.display = views.length <= 1 ? 'none' : ''
|
||||
// If the active view is not one this profile has (e.g. you were on Tree, then
|
||||
// switched to a two-pane profile), fall back to its first pane.
|
||||
if (!views.some(v => v.id === view)) view = views[0].id
|
||||
//
|
||||
// ALSO fall back when the profile CHANGED, even if the old view id happens to exist
|
||||
// here too. `view` is one global across profiles and 'map' is a valid id in nearly
|
||||
// every one, so an id-existence test alone let a stale 'map' survive a switch into a
|
||||
// profile whose FIRST pane is not the map (upg-100-list leads with Prayer Resources).
|
||||
// The column always opens at scroll position 0, so the strip then painted a pressed
|
||||
// tab that did not match the pane on screen. A new profile opens on its own first view.
|
||||
if (!views.some(v => v.id === view) || segProfile !== active) view = views[0].id
|
||||
segProfile = active
|
||||
$('#seg').innerHTML = views.map(v =>
|
||||
`<button data-v="${v.id}" aria-pressed="${v.id===view}">${icon(v.icon)}<span>${v.label}</span></button>`
|
||||
).join('')
|
||||
|
|
@ -826,10 +839,23 @@ const PROFILES = {
|
|||
// (index.js does import.meta.glob('./*.vue'), so poster-wall.vue registers
|
||||
// itself). Same tag, same src, different `profile` value: one download serves
|
||||
// both panes and they share data/clusters.json rather than fetching twice.
|
||||
// RESOURCES FIRST, MAP SECOND (Driver 2026-09-23). "By default the first tab
|
||||
// shouldn't be the map. The first tab should be the prayer resources" — that pane
|
||||
// already explains what a UPG-100 list IS at the top and shows the posters, so a
|
||||
// new user needs no explanation for it; a choropleth of zones does. Stated by him
|
||||
// as a general principle: "maps don't have to be primary. You can use lists and
|
||||
// grids for the primary view and maps as a secondary view… then they transition
|
||||
// into the map." Familiarity is the on-ramp.
|
||||
//
|
||||
// Both panes carry an EXPLICIT `config.profile`, including the map. The mount code
|
||||
// used to rely on "pane 0 is the map" positionally — reordering under that rule
|
||||
// would have mounted the map element into the poster pane. Naming the profile on
|
||||
// every pane is what makes the order data rather than convention.
|
||||
panes: [
|
||||
{ id:'map', label:'Map', at:[0,0] },
|
||||
{ id:'list', label:'Prayer Lists', at:[1,0],
|
||||
{ id:'list', label:'Prayer Resources', at:[0,0],
|
||||
config: { profile:'poster-wall', instanceId:'wrapper-upg-100' } },
|
||||
{ id:'map', label:'Map', at:[1,0],
|
||||
config: { profile:'upg-100-list', instanceId:'wrapper-upg-100' } },
|
||||
],
|
||||
},
|
||||
// COUNTRY POSTERS — one poster per country, the same shape as upg-100-list:
|
||||
|
|
@ -1046,13 +1072,25 @@ async function mount(id){
|
|||
slot.innerHTML = ''
|
||||
const column = buildPaneColumn(spec, { host: slot })
|
||||
if (column){
|
||||
// Pane 0 gets the element already built above; every OTHER pane that declares
|
||||
// its own `config` gets a sibling element from the SAME bundle (same tag, same
|
||||
// src — one download) with a different `profile`. A pane with no config keeps
|
||||
// its placeholder, so an unbuilt view never blocks the column.
|
||||
column.mountEl(column.panes[0].id)?.appendChild(el)
|
||||
// The element already built above goes to the pane that ASKED FOR ITS PROFILE;
|
||||
// every OTHER pane that declares its own `config` gets a sibling element from
|
||||
// the SAME bundle (same tag, same src — one download) with a different
|
||||
// `profile`. A pane with no config keeps its placeholder, so an unbuilt view
|
||||
// never blocks the column.
|
||||
//
|
||||
// WHY BY PROFILE, NOT BY INDEX. This used to be `panes[0]`, encoding "the map is
|
||||
// pane 0" as a positional convention. That silently breaks the moment a profile
|
||||
// wants a non-map pane first — upg-100-list now leads with its poster wall — and
|
||||
// the failure is the worst kind: the MAP element mounts into the POSTER pane and
|
||||
// both panes render the map. Panes that name no profile cannot match, so every
|
||||
// other multi-pane profile still lands on pane 0 exactly as before.
|
||||
const hostPane =
|
||||
column.panes.find(p => p.config?.profile && p.config.profile === spec.config?.profile)
|
||||
|| column.panes[0]
|
||||
column.mountEl(hostPane.id)?.appendChild(el)
|
||||
const extras = [el]
|
||||
for (const pane of column.panes.slice(1)){
|
||||
for (const pane of column.panes){
|
||||
if (pane.id === hostPane.id) continue
|
||||
if (!pane.config) continue
|
||||
const pel = document.createElement(spec.tag)
|
||||
const pcfg = { ...spec.config, ...pane.config }
|
||||
|
|
@ -1092,8 +1130,11 @@ async function mount(id){
|
|||
}
|
||||
column._showOnMap = (ev) => {
|
||||
const d = ev?.detail || {}
|
||||
// The map is pane 0 by convention (the element built above mounts there).
|
||||
const mapPaneId = column.panes[0]?.id
|
||||
// The map is the pane the SHELL-BUILT element mounted into — resolved by
|
||||
// profile, not by position. "Pane 0 is the map" stopped being true when
|
||||
// upg-100-list put its poster wall first; under the old rule "Show on map"
|
||||
// scrolled to the poster wall you clicked FROM, which looks like a dead button.
|
||||
const mapPaneId = hostPane?.id || column.panes[0]?.id
|
||||
if (mapPaneId) column.scrollTo(mapPaneId)
|
||||
// Forward to every live element in this column; the map profile picks it
|
||||
// up and selects the zone. A DESELECT arrives as listId === null, which
|
||||
|
|
|
|||
|
|
@ -83,6 +83,48 @@ export const STYLE_REGISTRY = [
|
|||
mapbox: 'mapbox://styles/mapbox/navigation-night-v1',
|
||||
maplibre: 'https://tiles.openfreemap.org/styles/dark',
|
||||
bestFor: ['research-dashboard'] },
|
||||
|
||||
// ── GALAXY — black ground, BLUE water ────────────────────────────────────────────────────
|
||||
// DRIVER 2026-09-23: "the map style needs to have the BLUE WATER with like the BLACK GALAXY
|
||||
// background". The continents go to deep space; the oceans stay unmistakably water.
|
||||
//
|
||||
// ★ 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
|
||||
// it would be MapLibre-only — breaking the registry's contract that every id resolves on BOTH
|
||||
// engines. Instead both providers start from their existing near-black style and we repaint
|
||||
// exactly the layers that carry water. The patch is small, auditable, and provider-symmetric.
|
||||
//
|
||||
// WHAT THE BASE STYLES ACTUALLY PAINT (measured 2026-09-23, not assumed):
|
||||
// OpenFreeMap dark — background rgb(12,12,12); layer id `water` (fill) is rgb(27,27,29) and
|
||||
// `waterway` (line) the same. i.e. the base style does not distinguish
|
||||
// water from land AT ALL. That is precisely the gap this entry fills.
|
||||
// Mapbox dark-v11 — its water layer ids are enumerated at runtime (they differ from
|
||||
// MapLibre's and have changed across style versions), so `paint` lists
|
||||
// CANDIDATES and the applier skips any id the loaded style lacks.
|
||||
//
|
||||
// THE BLUE: #2563a8 measures 3.19:1 against the rgb(12,12,12) ground — above the 3:1 WCAG
|
||||
// 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
|
||||
// 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:
|
||||
"I don't want a dark basemap, I didn't ask for one. I asked for a spherical GLOBE with a black
|
||||
background — the galaxy, like the Mapbox map — with BLUE OCEANS like the Mapbox map, and the
|
||||
land was NOT to be black."
|
||||
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
|
||||
blue oceans, and the blackness belongs to the SPACE BEHIND THE SPHERE, not to the ground.
|
||||
So the style is now a normal-coloured basemap with no paint overrides at all — the galaxy is
|
||||
not a property of the tiles. It is the void the globe floats in, which is the map CONTAINER's
|
||||
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',
|
||||
mapbox: 'mapbox://styles/mapbox/standard',
|
||||
// ★ FOUND, NOT AUTHORED (Driver: "I want it replaced with something you can find online").
|
||||
// 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
|
||||
// alternatives measured and rejected: CARTO Voyager's water is pale teal #b0d0d6; OSM
|
||||
// Americana has proper blue but its hosted tiles are a demo endpoint, not for production.
|
||||
maplibre: 'https://tiles.versatiles.org/assets/styles/colorful/style.json',
|
||||
bestFor: ['research-dashboard', 'my-upg-100-list'] },
|
||||
]
|
||||
|
||||
const BY_ID = Object.fromEntries(STYLE_REGISTRY.map(s => [s.id, s]))
|
||||
|
|
@ -92,16 +134,43 @@ const BY_ID = Object.fromEntries(STYLE_REGISTRY.map(s => [s.id, s]))
|
|||
* · a registry NAME → that entry's per-provider style URL
|
||||
* · a raw mapbox:// or http(s):// URL → passed through untouched (legacy behavior)
|
||||
* · anything else / undefined → undefined (caller keeps its own default)
|
||||
* Returns { style, lightPreset } — lightPreset only for named Standard variants.
|
||||
* Returns { style, lightPreset, paint } — lightPreset only for named Standard variants,
|
||||
* `paint` only for entries that patch the base style (see applyStylePaint).
|
||||
*/
|
||||
export function resolveNamedStyle(value, provider = 'mapbox') {
|
||||
if (typeof value !== 'string' || !value) return { style: undefined, lightPreset: undefined }
|
||||
if (typeof value !== 'string' || !value) return { style: undefined, lightPreset: undefined, paint: undefined }
|
||||
const entry = BY_ID[value]
|
||||
if (entry) {
|
||||
return provider === 'maplibre'
|
||||
? { style: entry.maplibre, lightPreset: undefined }
|
||||
: { style: entry.mapbox, lightPreset: entry.lightPreset }
|
||||
? { style: entry.maplibre, lightPreset: undefined, paint: entry.paint }
|
||||
: { style: entry.mapbox, lightPreset: entry.lightPreset, paint: entry.paint }
|
||||
}
|
||||
if (/^(mapbox:\/\/|https?:\/\/)/.test(value)) return { style: value, lightPreset: undefined }
|
||||
return { style: undefined, lightPreset: undefined }
|
||||
if (/^(mapbox:\/\/|https?:\/\/)/.test(value)) return { style: value, lightPreset: undefined, paint: undefined }
|
||||
return { style: undefined, lightPreset: undefined, paint: undefined }
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a registry entry's `paint` patch to a LOADED map, skipping any layer the style lacks.
|
||||
*
|
||||
* ★ WHY SKIP-IF-ABSENT IS THE WHOLE DESIGN. Layer ids are a property of the STYLE DOCUMENT, not
|
||||
* of the engine — Mapbox and MapLibre name water differently, and Mapbox has renamed its own ids
|
||||
* across style versions. Hardcoding one id and calling setPaintProperty on it throws and kills
|
||||
* the rest of the patch. Probing getLayer() first turns a wrong guess into a no-op, so a list
|
||||
* covering both engines is safe to apply to either.
|
||||
*
|
||||
* Returns the ids it actually matched, so a caller can log what the loaded style really had
|
||||
* rather than what we hoped it had.
|
||||
*/
|
||||
export function applyStylePaint(map, paint) {
|
||||
if (!map || !Array.isArray(paint)) return []
|
||||
const applied = []
|
||||
for (const p of paint) {
|
||||
if (!p || !p.layer || !p.prop) continue
|
||||
try {
|
||||
if (!map.getLayer(p.layer)) continue
|
||||
map.setPaintProperty(p.layer, p.prop, p.value)
|
||||
applied.push(p.layer + '.' + p.prop)
|
||||
} catch (_) { /* a style that has the layer but not the property: skip, never throw */ }
|
||||
}
|
||||
return applied
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,12 +147,24 @@ const FACET_TO_PROP = {
|
|||
// to join it: imb_gsec is a real row field with 5 {value,label} pairs and no value-vs-label
|
||||
// cross-collision, so valueMatches() selects the same rows whichever vocabulary arrives.
|
||||
gospel_access: 'gospelAccess',
|
||||
// ── THE TWO FORMERLY-DERIVED-ONLY AXES, wired at last (2026-09-23) ──
|
||||
// These were the "STILL DELIBERATELY UNMAPPED" pair above: their values are DERIVED
|
||||
// ("Hinduism" from the religion code's first letter; "Indo-European" from the
|
||||
// imb_language_family label or the ISO lookup), so no fetched row field could match them
|
||||
// and mapping them would have filtered the grid to ZERO. The grid now runs the SAME
|
||||
// derivations itself (people-groups-grid.vue DERIVED_AXES, importing the same @map/colors
|
||||
// + @map/data modules dashboard.config.js reads), so the value the dashboard emits and the
|
||||
// value the grid derives come from one owner and byte-match. With that in place, the top
|
||||
// tiers of the Religion and Language trees drive the grid like every other axis.
|
||||
religion_family: 'religionFamily',
|
||||
language_family: 'languageFamily',
|
||||
}
|
||||
|
||||
// Empty string (not null) is the grid's documented "no filter" value for every axis, so a
|
||||
// cleared facet must RESET its prop rather than leave the previous value stuck.
|
||||
const filter = reactive({ query: '', wagfRegion: '', wagfBlock: '', rop1: '', religion: '', countryCode: '',
|
||||
primaryLanguage: '', engagementStatus: '', cluster: '', peopleGroup: '', gospelAccess: '' })
|
||||
primaryLanguage: '', engagementStatus: '', cluster: '', peopleGroup: '', gospelAccess: '',
|
||||
religionFamily: '', languageFamily: '' })
|
||||
|
||||
function onDashboardFilter({ filterSet = {}, searchTerm = '' } = {}) {
|
||||
filter.query = searchTerm || ''
|
||||
|
|
@ -276,6 +288,8 @@ const sections = [
|
|||
:cluster="filter.cluster"
|
||||
:people-group="filter.peopleGroup"
|
||||
:gospel-access="filter.gospelAccess"
|
||||
:religion-family="filter.religionFamily"
|
||||
:language-family="filter.languageFamily"
|
||||
:show-search="false" :show-filters="false" :show-total="false" />
|
||||
</template>
|
||||
</StackLayout>
|
||||
|
|
|
|||
|
|
@ -274,38 +274,69 @@ const { scrimRef, scrimStyle } = useViewportScrim(exportState)
|
|||
// all stay — printPoster below still uses posterUrl(), and all three print paths still
|
||||
// share the exportState HUD for progress/errors.
|
||||
|
||||
// ─── SHARED PRINT-WINDOW MECHANISM ────────────────────────────────────────────
|
||||
// ─── SHARED PRINT MECHANISM ───────────────────────────────────────────────────
|
||||
// ONE mechanism, reused by all three "Print …" actions (poster / list / card
|
||||
// deck) rather than three ad-hoc window.opens. Extracted from the poster path,
|
||||
// which already carries the load-bearing fix documented below.
|
||||
//
|
||||
// ★ NO `noopener` HERE, AND THAT IS THE WHOLE FIX (2026-09-23). `noopener` makes window.open()
|
||||
// return null BY SPECIFICATION — severing the opener relationship is exactly what the flag does,
|
||||
// so there is no handle to hand back. With it, `w` was always null, the document.write preview
|
||||
// below was UNREACHABLE FOR EVERY USER, and every click fell through to a raw-content fallback.
|
||||
// MEASURED in a default (non-headless-flagged) browser: window.open('','_blank','noopener') -> null,
|
||||
// the identical call without the flag -> a usable handle. I first attributed the null to this
|
||||
// headless box; it is not environmental, it is what the flag means.
|
||||
// The window we write into carries same-origin markup we generate ourselves, so the opener
|
||||
// reference it regains is our own — there is nothing hostile for `noopener` to protect against here.
|
||||
// deck) rather than three ad-hoc implementations.
|
||||
//
|
||||
// `bodyHtml` is the FULL <html> document string (caller owns <head>/<style>/<body>). The caller's
|
||||
// content is responsible for its own print trigger — pass `autoprint:true` to have this helper
|
||||
// append the `window.focus();window.print()` bootstrap (used by markup with no <img onload> of
|
||||
// its own to hang the call off).
|
||||
//
|
||||
// ── HISTORY, kept because the lesson outlives the code that taught it ──
|
||||
// This helper used to call window.open(). An earlier build passed `noopener`, which makes
|
||||
// window.open() return null BY SPECIFICATION — severing the opener relationship is exactly what
|
||||
// the flag does, so there was no handle to write into. Every click silently fell through to a
|
||||
// raw-content fallback, for every user. MEASURED: window.open('','_blank','noopener') -> null;
|
||||
// the identical call without the flag -> a usable handle. It was first blamed on the headless box;
|
||||
// it was not environmental, it was what the flag means.
|
||||
// The iframe below makes that class of failure unreachable — there is no popup to block and no
|
||||
// opener flag to get wrong — but the lesson stands: a flag that returns null by spec will look
|
||||
// exactly like an environment problem.
|
||||
// ★ IN-PLACE (2026-09-23) — a hidden same-page IFRAME, not a popup window.
|
||||
//
|
||||
// WHY THIS CHANGED, so nobody "restores" the popup as a regression fix: an EARLIER Driver ask
|
||||
// wanted a preview window ("show the poster in your browser to help you print it"), and this
|
||||
// helper was built around window.open() to serve it. The Driver has now REVERSED that: the print
|
||||
// dialog must open DIRECTLY, with no second browser window. That is a decision change, not a bug.
|
||||
//
|
||||
// An iframe is the right vehicle because it keeps the ENTIRE existing contract: callers still pass
|
||||
// one full <html> document string, we still document.write() it, and the markup's own
|
||||
// `<img onload> -> window.print()` still fires — only now `window` inside that document is the
|
||||
// iframe's contentWindow, so the dialog belongs to THIS page. All three print paths (poster /
|
||||
// list / card deck) stay on this one mechanism.
|
||||
//
|
||||
// It also deletes a whole failure mode: there is no popup blocker on an iframe, so the
|
||||
// "popup blocked -> fall back to opening the raw image" branch every caller carried is now dead.
|
||||
// The returned object keeps a truthy `.document`-less shape only so existing `if (!w)` guards stay
|
||||
// harmless; callers should no longer rely on a popup-blocked null.
|
||||
function openPrintWindow(title, bodyHtml, { autoprint = false } = {}) {
|
||||
const w = window.open('', '_blank', 'width=980,height=1100')
|
||||
if (!w) return null // popup blocked — caller decides the fallback
|
||||
w.document.open()
|
||||
w.document.write(bodyHtml)
|
||||
const frame = document.createElement('iframe')
|
||||
// Off-screen rather than display:none — a display:none iframe does not lay out in every engine,
|
||||
// and a zero-height layout can print a blank first page.
|
||||
frame.setAttribute('aria-hidden', 'true')
|
||||
frame.style.cssText = 'position:fixed;right:0;bottom:0;width:1px;height:1px;opacity:0;border:0'
|
||||
document.body.appendChild(frame)
|
||||
|
||||
const doc = frame.contentDocument
|
||||
if (!doc) { frame.remove(); return null } // no document -> caller keeps its fallback
|
||||
doc.open()
|
||||
doc.write(bodyHtml)
|
||||
if (autoprint) {
|
||||
// Appended AFTER the caller's markup so it runs once the document is parsed —
|
||||
// matches the <img onload> timing the poster path already relies on.
|
||||
w.document.write(
|
||||
doc.write(
|
||||
'<script>window.addEventListener("load",function(){setTimeout(function(){window.focus();window.print()},150)})</' + 'script>')
|
||||
}
|
||||
w.document.close()
|
||||
return w
|
||||
doc.close()
|
||||
|
||||
// Reclaim the node once the dialog has been dismissed. `afterprint` is the honest signal; the
|
||||
// timeout is a floor for engines that never fire it, so a click can never leak a frame per press.
|
||||
const drop = () => { if (frame.parentNode) frame.remove() }
|
||||
try { frame.contentWindow.addEventListener('afterprint', () => setTimeout(drop, 0)) } catch { /* cross-doc guard */ }
|
||||
setTimeout(drop, 60000)
|
||||
|
||||
return frame.contentWindow
|
||||
}
|
||||
|
||||
// Escape the handful of characters that break out of an HTML text node or a "..." attribute.
|
||||
|
|
@ -336,20 +367,33 @@ async function printPoster(list) {
|
|||
const title = `Doxa UPG-100 poster — ${escapeHtml(list.label || list.id)}`
|
||||
const w = openPrintWindow(title,
|
||||
'<!DOCTYPE html><html><head><meta charset="utf-8"><title>' + title + '</title>' +
|
||||
'<style>@page{size:auto;margin:12mm}html,body{margin:0;height:100%;background:#fff}' +
|
||||
// PORTRAIT, explicitly. `size:auto` let the browser choose, and it chooses LANDSCAPE for a
|
||||
// square: the poster art is 3ft x 3ft (aspect-ratio:1/1, see the wall template below), so an
|
||||
// auto page has no reason to prefer the tall axis. The list and card-deck print paths in this
|
||||
// same file already pin `size:letter portrait`; the poster path was the odd one out.
|
||||
// (Driver: "print to a vertical piece of printer paper".)
|
||||
'<style>@page{size:letter portrait;margin:12mm}html,body{margin:0;height:100%;background:#fff}' +
|
||||
'body{display:flex;align-items:center;justify-content:center}' +
|
||||
'img{max-width:100%;max-height:100vh;object-fit:contain;display:block}' +
|
||||
'@media print{body{height:auto}img{max-height:none;width:100%}}</style></head>' +
|
||||
// A SQUARE on a PORTRAIT sheet must fit by its SHORTER axis. The old print rule
|
||||
// (`max-height:none;width:100%`) scaled the square to the full page WIDTH, which on portrait
|
||||
// letter is taller than the printable height -> the poster overflowed onto a second page.
|
||||
// Constrain BOTH axes and let object-fit letterbox it inside the sheet instead.
|
||||
'@media print{body{height:auto}' +
|
||||
'img{width:auto;max-width:100%;max-height:100%;object-fit:contain}}</style></head>' +
|
||||
'<body><img src="' + url + '" alt="' + title + '"' +
|
||||
' onload="setTimeout(function(){window.focus();window.print()},150)"' +
|
||||
' onerror="document.body.innerHTML=\'<p style="font:16px system-ui;padding:24px">Poster not available yet.</p>\'">' +
|
||||
'</body></html>')
|
||||
if (!w) { // popup blocked → open the image tab
|
||||
if (!w) {
|
||||
// No longer "popup blocked" — an iframe cannot be blocked. This is now only reachable if the
|
||||
// frame has no contentDocument at all (a sandboxed/detached host), so the honest fallback is
|
||||
// the poster itself rather than a pop-up instruction the user cannot act on.
|
||||
window.open(url, '_blank', 'noopener')
|
||||
exportState.value = { ...exportState.value, phase: 'done', message: 'opened poster', pct: 100 }
|
||||
setTimeout(() => { exportState.value = null }, 900); return
|
||||
}
|
||||
exportState.value = { ...exportState.value, phase: 'done', message: 'print preview opened', pct: 100 }
|
||||
exportState.value = { ...exportState.value, phase: 'done', message: 'print dialog opened', pct: 100 }
|
||||
setTimeout(() => { exportState.value = null }, 900)
|
||||
} catch (e) {
|
||||
exportState.value = { label: list.label, kind: 'Poster', phase: 'error',
|
||||
|
|
@ -433,11 +477,13 @@ async function printList100(list) {
|
|||
'<th>Religion</th><th></th></tr></thead><tbody>' + rowsHtml + '</tbody></table>' +
|
||||
'</body></html>', { autoprint: true })
|
||||
if (!w) {
|
||||
// Not a pop-up blocker any more (this prints via a same-page iframe). Only a host that
|
||||
// denies us a contentDocument lands here, so say something the user can act on.
|
||||
exportState.value = { label: list.label, kind: 'UPG-100 List', phase: 'error',
|
||||
message: 'Pop-up blocked — allow pop-ups for this site and try again.', pct: 0 }
|
||||
message: 'Could not open the print dialog in this view — try the Download button.', pct: 0 }
|
||||
return
|
||||
}
|
||||
exportState.value = { ...exportState.value, phase: 'done', message: 'print preview opened', pct: 100 }
|
||||
exportState.value = { ...exportState.value, phase: 'done', message: 'print dialog opened', pct: 100 }
|
||||
setTimeout(() => { exportState.value = null }, 900)
|
||||
} catch (e) {
|
||||
exportState.value = { label: list.label, kind: 'UPG-100 List', phase: 'error',
|
||||
|
|
@ -505,11 +551,12 @@ async function printCardDeck(list) {
|
|||
'<div class="deck">' + cardsHtml + '</div>' +
|
||||
'</body></html>', { autoprint: true })
|
||||
if (!w) {
|
||||
// See the note in the list path: an iframe cannot be pop-up blocked.
|
||||
exportState.value = { label: list.label, kind: 'Card Deck', phase: 'error',
|
||||
message: 'Pop-up blocked — allow pop-ups for this site and try again.', pct: 0 }
|
||||
message: 'Could not open the print dialog in this view — try the Download button.', pct: 0 }
|
||||
return
|
||||
}
|
||||
exportState.value = { ...exportState.value, phase: 'done', message: 'print preview opened', pct: 100 }
|
||||
exportState.value = { ...exportState.value, phase: 'done', message: 'print dialog opened', pct: 100 }
|
||||
setTimeout(() => { exportState.value = null }, 900)
|
||||
} catch (e) {
|
||||
exportState.value = { label: list.label, kind: 'Card Deck', phase: 'error',
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@
|
|||
* regions-dashboard could reuse it instead of copying it a third time.
|
||||
*
|
||||
* Copied conventions (see layouts/app-wrapper-material/index.html, 'upg-100-list'):
|
||||
* • panes: { id:'map', label:'Map', at:[0,0] } and
|
||||
* { id:'list', label:'Prayer Lists', at:[1,0], config:{ profile:'poster-wall' } }
|
||||
* • panes: { id:'list', label:'Prayer Resources', at:[0,0], config:{ profile:'poster-wall' } }
|
||||
* and { id:'map', label:'Map', at:[1,0] } — RESOURCES FIRST; see the note
|
||||
* on `panes` below for why the familiar view is the on-ramp to the map.
|
||||
* • per-pane element config = { ...spec.config, ...pane.config } — same tag, same
|
||||
* bundle, different `profile`; one download serves both panes.
|
||||
* • ONE shared instanceId for both panes (as the shell does) so the poster wall's
|
||||
|
|
@ -53,9 +54,21 @@ const spec = computed(() => ({
|
|||
id: 'upg-100-mfe',
|
||||
tag: TAG,
|
||||
config: { profile: 'upg-100-list', dataSource: cfg.value?.dataSource || 'pray-tools', instanceId: instanceId.value },
|
||||
// RESOURCES FIRST, MAP SECOND (Driver 2026-09-23). "By default the first tab
|
||||
// shouldn't be the map. The first tab should be the prayer resources" — that pane
|
||||
// already explains what a UPG-100 list IS at the top and shows the posters, so it
|
||||
// needs no explanation; a choropleth of zones does. Generalised by him as a design
|
||||
// principle: "maps don't have to be primary. You can use lists and grids for the
|
||||
// primary view and maps as a secondary view… then they transition into the map."
|
||||
//
|
||||
// ORDER LIVES IN `at[0]`, NOT IN THIS ARRAY'S ORDER. buildPaneColumn SORTS by
|
||||
// at[0] (pane-wrapper.js), while ViewSelect renders this array VERBATIM. Swapping
|
||||
// only one of the two desynchronises the tab strip from the scroll column — the
|
||||
// strip would read "Prayer Resources | Map" while scroll position 0 still held the
|
||||
// map. Both are swapped here, and `view` below is initialised from the same sort.
|
||||
panes: [
|
||||
{ id: 'map', label: 'Map', at: [0, 0], config: { profile: 'upg-100-list' } },
|
||||
{ id: 'list', label: 'Prayer Lists', at: [1, 0], config: { profile: 'poster-wall' } },
|
||||
{ id: 'list', label: 'Prayer Resources', at: [0, 0], config: { profile: 'poster-wall' } },
|
||||
{ id: 'map', label: 'Map', at: [1, 0], config: { profile: 'upg-100-list' } },
|
||||
],
|
||||
// The View switcher lives in this element's own header, so the layout's
|
||||
// "… below ↓" pill would be a second, redundant control (shell design note 2026-08-01).
|
||||
|
|
@ -63,7 +76,24 @@ const spec = computed(() => ({
|
|||
}))
|
||||
|
||||
const stage = ref(null)
|
||||
const view = ref('map')
|
||||
// DERIVED from the pane order, never a hardcoded id. This was `ref('map')`, which
|
||||
// silently became wrong the moment the panes were reordered: the column scrolled to
|
||||
// the resources pane while the strip still painted "Map" as pressed. Sorting by
|
||||
// at[0] here is the SAME rule buildPaneColumn applies, so the pressed tab and the
|
||||
// pane at scroll-position 0 cannot disagree.
|
||||
const firstPaneId = computed(() => {
|
||||
const col = spec.value.panes
|
||||
.filter(p => (p.at?.[1] ?? 0) === 0)
|
||||
.sort((a, b) => (a.at?.[0] ?? 0) - (b.at?.[0] ?? 0))
|
||||
return col[0]?.id || 'list'
|
||||
})
|
||||
const viewRaw = ref(null)
|
||||
// Until the column exists, the active view IS the first pane — so the strip paints a
|
||||
// pressed tab on the very first render rather than flashing with nothing selected.
|
||||
const view = computed({
|
||||
get: () => viewRaw.value || firstPaneId.value,
|
||||
set: (v) => { viewRaw.value = v },
|
||||
})
|
||||
let column = null
|
||||
let offScroll = null
|
||||
|
||||
|
|
|
|||
|
|
@ -45,13 +45,25 @@ import Fuse from '@map/components/search/fuzzy.js'
|
|||
import SearchBar from '@map/components/search/SearchBar.vue'
|
||||
import FilterDropdown from '@map/components/search/FilterDropdown.vue'
|
||||
import { countrySlug } from './src/countries.js'
|
||||
// ── DERIVED family axes (BUG-SEMANTIC-TREE-SIDEBAR-NOT-INTEROPERABLE-WITH-GRID) ──
|
||||
// The Religion/Language trees' TOP TIERS emit DERIVED family names ("Hinduism",
|
||||
// "Indo-European") that exist on no row field, so for months they could narrow the dashboard
|
||||
// but never this grid. The fix is to run the SAME derivation here, from the SAME library
|
||||
// modules the dashboard's config uses -- one owner per derivation, two consumers:
|
||||
import { RELIGION_FAMILIES } from '@map/colors/religion.js'
|
||||
import { canonicalFamilyName } from '@map/colors/language-family.js'
|
||||
import langFamilyByIso from '@map/data/langFamilyByIso.json'
|
||||
|
||||
const props = defineProps({
|
||||
// ── injected standalone deps (replace Nuxt useRuntimeConfig / useI18n / useLocalePath) ──
|
||||
profileConfig: { type: String, default: '' },
|
||||
prayBaseUrl: { type: String, default: '' },
|
||||
languageCode: { type: String, default: '' },
|
||||
researchUrl: { type: String, default: '/research/' },
|
||||
// ★ default '' NOT '/research/': a non-empty prop default is ALWAYS truthy, so the computed
|
||||
// below could never reach its canonical fallback -- the relative default simply moved houses
|
||||
// and kept winning (measured: bundle carried the canonical branch AND default:"/research/",
|
||||
// and every rendered link was still relative). Unset must mean unset. */
|
||||
researchUrl: { type: String, default: '' },
|
||||
selectUrl: { type: String, default: '' },
|
||||
t: { type: Object, default: () => ({}) },
|
||||
|
||||
|
|
@ -179,6 +191,9 @@ const props = defineProps({
|
|||
// a REAL row field, unlike the derived language_family/religion_family facets, which is why it
|
||||
// can move the grid at all.
|
||||
gospelAccess: { type: String, default: '' },
|
||||
// ── the two DERIVED family axes (no row field carries them; see DERIVED_AXES below) ──
|
||||
religionFamily: { type: String, default: '' },
|
||||
languageFamily: { type: String, default: '' },
|
||||
|
||||
// ── presentation knobs copied from UupgsList (defaults preserve today's behaviour) ──
|
||||
perPage: { type: Number, default: 24 },
|
||||
|
|
@ -194,7 +209,18 @@ const cfg = computed(() => {
|
|||
})
|
||||
const prayBaseUrl = computed(() => props.prayBaseUrl || cfg.value.prayBaseUrl || 'https://pray.doxa.life')
|
||||
const languageCode = computed(() => props.languageCode || cfg.value.languageCode || '')
|
||||
const researchUrl = computed(() => props.researchUrl || cfg.value.researchUrl || '/research/')
|
||||
// ★ ABSOLUTE CANONICAL DEFAULT (Driver 2026-09-23, pasted from his own browser:
|
||||
// http://localhost:5174/research/pitaguari/ -- "when I click view more information it does not
|
||||
// send me to the proper doxa.life page"). The old default '/research/' was ROOT-RELATIVE, so it
|
||||
// resolved against whatever origin happened to host the element: right on doxa.life, wrong on
|
||||
// every other host -- the dev server, an iframe embed, a partner site. Same defect class as the
|
||||
// ../../ child-src bug: a relative default encoding an assumption about the host.
|
||||
// The shared PeopleGroupDetail already solved this exact problem (PeopleGroupDetail.vue:427):
|
||||
// window.DOXA_SITE_URL override, else the absolute canonical origin. Adopting the SAME pattern
|
||||
// here makes the card link and the detail panel agree on where a people group lives -- two
|
||||
// surfaces, one rule. Prop and cfg still take precedence for hosts that want their own base.
|
||||
const researchUrl = computed(() => props.researchUrl || cfg.value.researchUrl
|
||||
|| (((typeof window !== 'undefined' && window.DOXA_SITE_URL) || 'https://doxa.life').replace(/\/+$/, '') + '/research/'))
|
||||
const selectUrl = computed(() => props.selectUrl || cfg.value.selectUrl || '')
|
||||
const t = computed(() => ({ ...(cfg.value.t || {}), ...(props.t || {}) }))
|
||||
|
||||
|
|
@ -254,6 +280,10 @@ const propFilters = computed(() => {
|
|||
// The API-field-name rule a fourth time, for the same reason: the prop is `gospelAccess` and the
|
||||
// row field is `imb_gsec`. Keying on the prop name would read undefined on every row.
|
||||
if (props.gospelAccess) f.imb_gsec = { value: props.gospelAccess, label: lbl(props.gospelAccess) }
|
||||
// DERIVED axes: the key is not a row field but a DERIVED_AXES accessor name -- rowAxisValue()
|
||||
// resolves it, so both filter loops stay on the one shared matcher.
|
||||
if (props.religionFamily) f.__religion_family = { value: props.religionFamily, label: lbl(props.religionFamily) }
|
||||
if (props.languageFamily) f.__language_family = { value: props.languageFamily, label: lbl(props.languageFamily) }
|
||||
return f
|
||||
})
|
||||
const activeFilters = computed(() => (useFilters.value ? localFilters.value : propFilters.value))
|
||||
|
|
@ -356,6 +386,34 @@ function toggleExact() {
|
|||
// vocabulary works and a new value needs no code change. Kept deliberately narrow: it only
|
||||
// widens what counts as equal, so a genuine non-match still filters to nothing.
|
||||
const norm = (s) => String(s ?? '').toLowerCase().replace(/[\s_&-]+/g, '')
|
||||
// ── DERIVED AXES: family values computed per-row, byte-parity with dashboard.config.js ──
|
||||
// religion family = first letter of the religion CODE -> RELIGION_FAMILIES name; language
|
||||
// family = the API's imb_language_family label when present (1.6% of rows), else the
|
||||
// ISO-639-3 lookup via langFamilyByIso (lifts coverage to ~96%), both case-folded through
|
||||
// canonicalFamilyName. Returned as {value,label} so valueMatches() treats a derived axis
|
||||
// exactly like a fetched one.
|
||||
const DERIVED_AXES = {
|
||||
__religion_family: (u) => {
|
||||
const code = u.religion && typeof u.religion === 'object' ? u.religion.value : u.religion
|
||||
const c = String(code || '').charAt(0).toUpperCase()
|
||||
const name = c && RELIGION_FAMILIES[c] ? RELIGION_FAMILIES[c] : ''
|
||||
return name ? { value: name, label: name } : null
|
||||
},
|
||||
__language_family: (u) => {
|
||||
const f = u.imb_language_family
|
||||
const api = f && typeof f === 'object' ? (f.label || f.value) : f
|
||||
let name = ''
|
||||
if (api) name = canonicalFamilyName(api)
|
||||
else {
|
||||
const iso = u.primary_language && typeof u.primary_language === 'object' ? u.primary_language.value : null
|
||||
const hit = iso && langFamilyByIso[iso]
|
||||
name = hit ? canonicalFamilyName(hit) : ''
|
||||
}
|
||||
return name ? { value: name, label: name } : null
|
||||
},
|
||||
}
|
||||
// ONE resolver for both filter loops: a derived key runs its accessor, a real key reads the row.
|
||||
const rowAxisValue = (u, field) => (DERIVED_AXES[field] ? DERIVED_AXES[field](u) : u[field])
|
||||
function valueMatches(vl, selection) {
|
||||
if (!vl) return false
|
||||
const want = norm(selection?.value)
|
||||
|
|
@ -367,7 +425,7 @@ function applyDropdownFilters(list) {
|
|||
let filtered = list
|
||||
for (const [field, selection] of Object.entries(activeFilters.value)) {
|
||||
if (field === 'exact') { filtered = filtered } else {
|
||||
filtered = filtered.filter((u) => valueMatches(u[field], selection))
|
||||
filtered = filtered.filter((u) => valueMatches(rowAxisValue(u, field), selection))
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
|
|
@ -435,7 +493,7 @@ function applyWith(filters, list) {
|
|||
let filtered = list
|
||||
for (const [field, selection] of Object.entries(filters)) {
|
||||
if (field === 'exact') continue
|
||||
filtered = filtered.filter((u) => valueMatches(u[field], selection))
|
||||
filtered = filtered.filter((u) => valueMatches(rowAxisValue(u, field), selection))
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
|
@ -569,7 +627,7 @@ async function getUUPGs() {
|
|||
// primaryLanguage prop match nothing and silently filter the grid to zero rows. Verified live
|
||||
// (2026-09-22): adding it keeps the response at 2,107 rows and the field is populated on 100%
|
||||
// of them. Any future filter axis must be added HERE as well as to propFilters.
|
||||
const url = `${prayBaseUrl.value}/api/people-groups/list?fields=name,slug,wagf_region,wagf_block,country_code,rop1,religion,has_photo,image_url,adopted_by_churches,imb_alternate_name,engagement_status,people_committed,primary_language,imb_reg_of_people_2,imb_reg_of_people_25,imb_gsec&lang=${languageCode.value}`
|
||||
const url = `${prayBaseUrl.value}/api/people-groups/list?fields=name,slug,wagf_region,wagf_block,country_code,rop1,religion,has_photo,image_url,adopted_by_churches,imb_alternate_name,engagement_status,people_committed,primary_language,imb_reg_of_people_2,imb_reg_of_people_25,imb_gsec,imb_language_family&lang=${languageCode.value}`
|
||||
loading.value = true
|
||||
loadFailed.value = false
|
||||
try {
|
||||
|
|
@ -604,8 +662,14 @@ async function getUUPGs() {
|
|||
}
|
||||
|
||||
// external prop-driven filters should re-filter the grid (when controls are hidden).
|
||||
watch(() => [props.query, props.wagfRegion, props.wagfBlock, props.rop1, props.religion, props.countryCode,
|
||||
props.primaryLanguage, props.engagementStatus, props.cluster, props.peopleGroup, props.gospelAccess,
|
||||
// ★ WATCH THE DERIVED propFilters, NOT a hand-enumerated prop list. This watcher used to spell
|
||||
// out every filter prop by name -- the THIRD copy of the axis list (defineProps, propFilters,
|
||||
// here), and the copy nobody remembers: religionFamily/languageFamily landed in the first two
|
||||
// and the grid received the prop, showed its "Showing" count line, and silently never
|
||||
// re-filtered (measured: Hinduism selected, first card still a Malaysian Islamic group).
|
||||
// propFilters already derives from every axis prop, so watching it makes the axis list have
|
||||
// ONE owner and a future axis cannot miss the watcher.
|
||||
watch(() => [props.query, propFilters.value,
|
||||
useSearch.value, useFilters.value], () => {
|
||||
page.value = 1
|
||||
filterUUPGs()
|
||||
|
|
|
|||
|
|
@ -90,18 +90,12 @@ export default {
|
|||
// family totals sum exactly to the leaf-code totals.
|
||||
religion_family: {
|
||||
label: 'Religion Family',
|
||||
// ★ DASHBOARD-ONLY, AND SAYING SO IS THE FIX. This facet narrows the dashboard correctly but
|
||||
// can never move the grid beneath it, because the value it emits ("Hinduism") is a DERIVED
|
||||
// family name that exists on no row field — the rows carry religion CODES. Mapping it in
|
||||
// dashboard-plus-grid's FACET_TO_PROP would compare "Hinduism" against a code and filter the
|
||||
// grid to ZERO, which is worse than leaving it unmapped.
|
||||
//
|
||||
// Measured 2026-09-23 (interop sweep, 4 rows tested): dashboard 2107 -> 693/649/610/83, grid
|
||||
// unchanged every time. That combination is indistinguishable from a broken button: the row
|
||||
// highlights, the numbers above move, and the list below ignores you. The honest fix is not to
|
||||
// wire it — it is to TELL the user this row drives the dashboard only, which is what this flag
|
||||
// makes the rail render.
|
||||
dashboardOnly: true,
|
||||
// ★ NO LONGER dashboard-only (2026-09-23). This facet's value is DERIVED ("Hinduism" from
|
||||
// the religion code's first letter), which for months meant no grid row field could match
|
||||
// it -- the honest stopgap was a dashboardOnly flag the rail rendered as "updates this
|
||||
// dashboard only". The real fix landed instead: people-groups-grid runs the SAME
|
||||
// derivation (DERIVED_AXES, importing the same RELIGION_FAMILIES map this file reads), so
|
||||
// the family name the bus carries now byte-matches a value the grid computes per row.
|
||||
get: (r) => {
|
||||
const code = r.religion && typeof r.religion === 'object' ? r.religion.value : r.religion
|
||||
const c = String(code || '').charAt(0).toUpperCase()
|
||||
|
|
@ -155,11 +149,10 @@ export default {
|
|||
// byte-match contract intact.
|
||||
language_family: {
|
||||
label: 'Language Family',
|
||||
// ★ DASHBOARD-ONLY — same class as religion_family, different cause. This value is DERIVED
|
||||
// via langFamilyByIso for ~96% of rows, so it exists on no row field the grid can filter on.
|
||||
// Measured 2026-09-23 (4 rows tested): dashboard 2107 -> 677/306/222/151, grid unchanged.
|
||||
// See the religion_family note above for why declaring it beats wiring it.
|
||||
dashboardOnly: true,
|
||||
// ★ NO LONGER dashboard-only (2026-09-23) — same arc as religion_family above: the value
|
||||
// is DERIVED (imb_language_family label, else langFamilyByIso over the ISO code), and the
|
||||
// grid now runs the SAME derivation from the SAME @map modules (DERIVED_AXES), so the
|
||||
// Language tree's top tier drives the grid like every fetched axis.
|
||||
get: (r) => {
|
||||
const api = lbl(r.imb_language_family)
|
||||
if (api) return canonicalFamilyName(api)
|
||||
|
|
@ -310,7 +303,38 @@ export default {
|
|||
// POINT data", which is exactly this map's job, so this is an alignment, not a new choice.
|
||||
// One line, and it survives the MapLibre swap unchanged.
|
||||
map: {
|
||||
mapStyle: 'paper',
|
||||
// ── PROVIDER: MapLibre ───────────────────────────────────────────────────────────────────
|
||||
// DRIVER 2026-09-22: "the map on the big picture to be MapLibre"; and 2026-09-23 on the
|
||||
// terms — "keep every Mapbox component working so we don't break Mapbox compatibility — we
|
||||
// just want interoperability, so we can come back to Mapbox if this breaks things."
|
||||
//
|
||||
// ★ THIS ONE KEY IS THE WHOLE SWAP, AND THAT IS THE POINT. DashboardMapPanel reads it
|
||||
// (`cfg.provider === 'maplibre' ? 'maplibre' : 'mapbox'`), useMapInstance loads the matching
|
||||
// engine + CSS, and registry.js resolves the style name on whichever engine is live. Delete
|
||||
// this line — or set it to 'mapbox' — and the dashboard is byte-for-byte what it was before,
|
||||
// with the Mapbox token still wired and no other file to restore. Reversibility is not a
|
||||
// claim here; it is the shape of the change. (Proven by flipping it and rebuilding, not
|
||||
// asserted — see the verification note in the commit.)
|
||||
//
|
||||
// WHY IT COSTS NOTHING TO STAY DUAL: MapLibre ignores accessToken rather than rejecting it,
|
||||
// so the Mapbox credential stays threaded through untouched while MapLibre is live.
|
||||
// ★ MAPBOX FOR THIS MAP, BY THE DRIVER'S OWN DESCRIPTION (2026-09-23). Every wording of the
|
||||
// ask pointed at one thing: "like the Mapbox map", "a mirror or clone of Mapbox's globe space
|
||||
// style with the stars", "water like Mapbox style". Mapbox Standard's globe NATIVELY renders
|
||||
// the starfield, the atmosphere and that water -- no clone can beat the original, and we hold
|
||||
// a working baked token (the Driver's explicit ruling accepted it as public). MapLibre support
|
||||
// stays fully built and reversible: flip this one key back and the keyless VersaTiles path
|
||||
// returns. Interoperability was the requirement; it is intact.
|
||||
provider: 'mapbox',
|
||||
|
||||
// 'galaxy' = "deep-space ground, blue oceans" (library/styles/registry.js).
|
||||
// DRIVER 2026-09-23: "the map style needs to have the BLUE WATER with like the BLACK GALAXY
|
||||
// background". It resolves on BOTH engines — MapLibre's keyless OpenFreeMap dark and Mapbox
|
||||
// dark-v11 — each patched to the same blue water at style.load, so flipping `provider` above
|
||||
// does not change how the map LOOKS, only which engine draws it. That is what makes the
|
||||
// reversibility test above meaningful rather than cosmetic.
|
||||
// Was 'paper' (near-white minimal) until this change.
|
||||
mapStyle: 'galaxy',
|
||||
|
||||
// ── HEARTBEAT ZONES ──────────────────────────────────────────────────────────────────────
|
||||
// A HEARTBEAT ZONE is a country holding the most people groups — where the concentration of
|
||||
|
|
@ -371,7 +395,16 @@ export default {
|
|||
|
||||
// ── charts ── (DashboardChartsRail)
|
||||
charts: [
|
||||
{ id: 'religion', type: 'donut', facet: 'religion', title: 'Religion', topN: 8 },
|
||||
// ★ HIGH-LEVEL RELIGIONS ONLY (Driver 2026-09-23: "it should not mention Ethnoreligion and
|
||||
// Islam multiple times, just once -- just the high-level religions, not broken down into
|
||||
// smaller categories"). The rollup ALREADY EXISTED as the religion_family facet ("Islam",
|
||||
// "Hinduism", "Ethnoreligion" -- one row each); the chart was simply pointed at the
|
||||
// fine-grained `religion` facet ("Islam - Sunni", "Islam - Folk", ...). One word moves it.
|
||||
// religion_family is dashboardOnly by design (a derived value no row field carries), so a
|
||||
// slice click narrows the DASHBOARD but not the grid below -- documented behaviour, and for
|
||||
// a glanceable donut that is the right trade. The full breakdown still lives in the facet
|
||||
// rail's Religion tree for anyone who needs Sunni vs Folk.
|
||||
{ id: 'religion', type: 'donut', facet: 'religion_family', title: 'Religion', topN: 8 },
|
||||
{ id: 'region', type: 'donut', facet: 'wagf_region', title: 'Region', topN: 9 },
|
||||
{ id: 'country', type: 'bar', facet: 'country_code', title: 'Top countries', topN: 12 },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -180,11 +180,15 @@ const { startFacetResize, startRailResize, startTableResize, startShellResize, o
|
|||
// mapH mirrors the page-scroll CSS fallback 40svh (~360px at a 900px viewport — see
|
||||
// BUG-DASHBOARD-BAND-GROWS-ENORMOUS: 58svh left the default band taller than one viewport;
|
||||
// 40svh is the value that makes chrome(137px) + map(X) + table(X+32px bar) sum to <=900px).
|
||||
// shellH default 720 = the map+table combined budget that mapH:360 used to imply on its own
|
||||
// (360 map + 360 table-with-bar) — see useDashboardLayout.js's DEFAULTS comment for the exact
|
||||
// accounting. Must mirror --rd-shell-h's CSS fallback below for the same reset()-vs-fresh-load
|
||||
// reason as the other three.
|
||||
defaults: { facetW: 296, railW: 264, tableH: '34%', mapH: 360, shellH: 720 },
|
||||
// ★ NO LOCAL defaults OVERRIDE — deliberately. This line used to pass its own copy of the
|
||||
// numbers ({ mapH:360, shellH:720 }), which silently OVERRODE the composable's DEFAULTS via the
|
||||
// { ...DEFAULTS, ...options.defaults } merge. Result, measured 2026-09-23: the composable said
|
||||
// mapH 260 / shellH 502, this line said 360 / 720, the bundle carried BOTH, and this one won --
|
||||
// so two height-reclaim passes changed the shell by exactly nothing while every CSS fallback
|
||||
// moved. The one-quantity-one-owner rule, third costume: a duplicated DEFAULT is still a second
|
||||
// owner. The composable's DEFAULTS (useDashboardLayout.js:98, with the row arithmetic comment)
|
||||
// is the single home; anything this host needs to differ must be a conscious, single-key
|
||||
// override with a reason, never a full mirrored copy that drifts.
|
||||
})
|
||||
|
||||
// KPI strip — generic formatting per kind (mirrors the original inline strip).
|
||||
|
|
@ -223,6 +227,14 @@ useShadowStyles(`
|
|||
_qa-cycle/01-ideation-research/REFERENCE-slim-material-ui-standards.md, REF-slim-material-ui-standards).
|
||||
LIGHT ONLY — there is no dark mode in this product and none may be added.
|
||||
|
||||
★ SCOPE: this law governs UI CHROME — panels, cards, type, borders, controls; everything
|
||||
whose colour comes from a token above. It does NOT govern BASEMAP TILES. The map panel
|
||||
renders a dark basemap ('galaxy' — black ground, blue water, set in dashboard.config.js on
|
||||
the Driver's 2026-09-23 ruling), and that is DELIBERATE, not drift: a basemap is imagery the
|
||||
data is drawn ON, chosen for choropleth contrast, not a surface the UI is themed with. A
|
||||
dark map inside light chrome is the intended composition. Do not "fix" it to match the
|
||||
palette, and do not cite it as precedent for darkening any chrome.
|
||||
|
||||
--dx-scroll-thumb is a DELIBERATE neutral, not a sage: it must read between line and muted
|
||||
on both white and surface, which a sage tint does not do. It is named, therefore chosen. */
|
||||
:host{ display:block; width:100%; height:100%; min-height:560px; box-sizing:border-box;
|
||||
|
|
@ -244,7 +256,10 @@ useShadowStyles(`
|
|||
ONE line, while the charts carry a donut plus a legend that can wrap.
|
||||
So the widths SWAP ROLES: the tree takes the wider column, the charts the narrower.
|
||||
The pair still sums to 560px, so the centre map cell is unchanged. */
|
||||
--facet-w:296px; --rail-w:264px;
|
||||
/* ★ SYMMETRIC SIDEBARS (Driver 2026-09-23: "the charts sidebar can be wider so it is the
|
||||
same width as the left sidebar, which will put the map in the exact center"). rail
|
||||
264 -> 296 = facet width; equal flanks centre the middle column by construction. */
|
||||
--facet-w:296px; --rail-w:296px;
|
||||
/* ── ★ ONE EDGE GUTTER FOR THE WHOLE HEADER STACK ────────────────────────────────────
|
||||
(card BUILD-THE-FOUR-HEADERS-ARE-ONE-CENTRED-STACK: "on the edges there should be padding
|
||||
so that everything lines up perfectly.")
|
||||
|
|
@ -272,14 +287,14 @@ useShadowStyles(`
|
|||
failure mode --rd-edge was introduced to prevent for the horizontal gutter. */
|
||||
/* ★ HEIGHT RECLAIM 2026-09-23: topbar 52->44, stats 30->26. Both are now VARS so a host can
|
||||
tune them without editing three grid-template-rows declarations that must agree. */
|
||||
--rd-topbar-h:44px;
|
||||
--rd-stats-h:26px;
|
||||
--rd-topbar-h:38px;
|
||||
--rd-stats-h:22px;
|
||||
--fs-xs:10px; --fs-sm:11px; --fs-md:12px; --fs-lg:13px; --fs-xl:16px;
|
||||
--r-lg:10px; --r-md:6px; --r-pill:999px; }
|
||||
.profile-loader{ width:100%; height:100%; }
|
||||
.rd-shell{ position:relative; display:grid; width:100%; height:100%; min-height:560px; box-sizing:border-box;
|
||||
grid-template-columns: var(--facet-w,296px) minmax(0,1fr) var(--rail-w,264px);
|
||||
grid-template-rows: var(--rd-topbar-h,44px) auto var(--rd-stats-h,26px) 1fr var(--table-h,34%);
|
||||
grid-template-columns: var(--facet-w,296px) minmax(0,1fr) var(--rail-w,296px);
|
||||
grid-template-rows: var(--rd-topbar-h,38px) auto var(--rd-stats-h,22px) 1fr var(--table-h,34%);
|
||||
grid-template-areas: "topbar topbar topbar" "filters filters filters" "stats stats stats" "facets map rail" "facets table rail";
|
||||
gap:1px; background:var(--rd-line, #d1d4cd); container-type:inline-size;
|
||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
|
||||
|
|
@ -297,11 +312,11 @@ useShadowStyles(`
|
|||
still showing the live matched count and the Export CSV button (card
|
||||
BUILD-CSV-EXPORT-MOVE-TO-TABLE-HEADER-ALL-FIELDS). The row shrinks to the bar's height
|
||||
instead of 0, so the map still takes back nearly the whole cell via its 1fr. */
|
||||
.rd-shell.rd-table-collapsed{ grid-template-rows:var(--rd-topbar-h,44px) auto var(--rd-stats-h,26px) 1fr var(--table-bar-h,30px); }
|
||||
.rd-shell.rd-table-collapsed{ grid-template-rows:var(--rd-topbar-h,38px) auto var(--rd-stats-h,22px) 1fr var(--table-bar-h,30px); }
|
||||
/* Facet/rail collapse — override the COLUMN template (not the --*-w vars) so collapse wins
|
||||
over a dragged inline width, exactly like the table row-template above. Map is minmax(0,1fr)
|
||||
and self-resizes via DashboardMapPanel's ResizeObserver → map.resize(). */
|
||||
.rd-shell.rd-facet-collapsed{ grid-template-columns:0 minmax(0,1fr) var(--rail-w,264px); }
|
||||
.rd-shell.rd-facet-collapsed{ grid-template-columns:0 minmax(0,1fr) var(--rail-w,296px); }
|
||||
.rd-shell.rd-rail-collapsed{ grid-template-columns:var(--facet-w,296px) minmax(0,1fr) 0; }
|
||||
.rd-shell.rd-facet-collapsed.rd-rail-collapsed{ grid-template-columns:0 minmax(0,1fr) 0; }
|
||||
/* Always-reachable edge carets pinned to the map, toggle each rail (desktop only). */
|
||||
|
|
@ -388,8 +403,60 @@ useShadowStyles(`
|
|||
shell budget). Every derived measurement below (table budget, facets/rail
|
||||
span) now roots in --rd-shell-h so the total stays the single source of
|
||||
truth and the two handles cannot fight over the same pixels. */
|
||||
.rd-shell.rd-page-scroll{ height:auto; --table-h:auto; }
|
||||
.rd-shell.rd-page-scroll > [data-region="map"]{ height:var(--rd-map-h, 40svh); overflow:hidden; }
|
||||
/* ★ THE SHELL TOTAL NOW DERIVES FROM THE TABLE, NOT THE OTHER WAY ROUND (regression I introduced
|
||||
in a693865 and the Driver caught immediately: "the height of the table is smaller now, but that
|
||||
introduced a GAP, a grey spot ... which also broke the ability to resize the table").
|
||||
WHAT I BROKE. The design was one budget with two consumers: --rd-shell-h was the total, and the
|
||||
table's keyhole was DERIVED as shell - map - bar - foot. Capping the keyhole at 7 rows replaced
|
||||
that derivation with an independent constant, so the table stopped answering to the shell. The
|
||||
shell kept asserting its old ~720px total while the table gave back 118px, and since nothing
|
||||
else consumed the slack it showed up as dead space — the grey spot. The resize handle broke for
|
||||
the same reason: it writes --rd-shell-h, which the table no longer reads, so dragging moved a
|
||||
number that nothing downstream obeyed.
|
||||
THE FIX IS TO RESTORE ONE SOURCE OF TRUTH, but pointing the other way: the TABLE's seven rows
|
||||
are now the declared quantity and --rd-shell-h is COMPUTED from it (map + bar + rows + foot).
|
||||
Both handles keep working because both still write inputs to that sum: the map/table divider
|
||||
writes --rd-map-h, and the shell handle writes --rd-shell-h, which overrides this default.
|
||||
★ The var indirection matters: an explicit --rd-shell-h from the drag still wins, so this is a
|
||||
DEFAULT, not a lock. That is what keeps the resize alive rather than pinning the element. */
|
||||
/* ★ THE MAP ROW MUST NOT BE 1fr IN PAGE-SCROLL — this is the grey gap, found by measurement.
|
||||
The base grid (line 290) sets the map row to 1fr, which is right in FIXED-VIEWPORT: the shell
|
||||
has a real bounded height and 1fr means "take what is left". In PAGE-SCROLL the shell is
|
||||
height:auto, so there is no fixed total to divide — 1fr instead becomes "stretch to whatever
|
||||
the content implies", and the map ELEMENT is separately pinned to --rd-map-h. The row grows,
|
||||
the element does not, and the difference paints as dead space between the map and the table.
|
||||
MEASURED in a fresh context: shell 849 while its parts sum to 602 — a 117px gap, exactly the
|
||||
"grey spot between the height of the map" the Driver reported twice.
|
||||
Pinning the row to the SAME var the element uses makes row and element one quantity again, so
|
||||
the slack cannot exist. Both drags keep working because both write that var (or --rd-shell-h,
|
||||
which the table's keyhole below derives from). */
|
||||
/* ★ THE GREY GAP BETWEEN MAP AND TABLE = THE 1fr ROW STRETCHING PAST ITS PINNED OCCUPANT.
|
||||
The base grid gives the map row 1fr, which is correct in fixed-viewport (a real bounded total
|
||||
to divide) and wrong in page-scroll, where the shell is height:auto: 1fr then stretches to
|
||||
whatever the content implies while the map ELEMENT stays pinned at --rd-map-h. The row grows,
|
||||
the element does not, and the difference paints as dead space above the table — reported by
|
||||
the Driver three separate times. Pinning the ROW to the same var the ELEMENT uses makes them
|
||||
one quantity, so the slack cannot exist; the table row goes auto for the same reason.
|
||||
★ HISTORY, so this does not get reverted on a false signal again: this exact rule was applied
|
||||
earlier today and reverted when a probe reported "no shell" — the real cause was a DEAD DEV
|
||||
SERVER, not this CSS. Verified this time against a live server with a screenshot. */
|
||||
.rd-shell.rd-page-scroll{ height:auto; --table-h:auto;
|
||||
grid-template-rows: var(--rd-topbar-h,38px) auto var(--rd-stats-h,22px) var(--rd-map-h,300px) auto; }
|
||||
/* ★ COLLAPSE GIVES ITS SPACE TO THE MAP (Driver 2026-09-23: "collapsing the table is supposed to
|
||||
collapse down and increase the size of the map -- instead it collapsed up and introduced white
|
||||
space under the collapsed table"). The base .rd-table-collapsed template still says 1fr for the
|
||||
map row, and 1fr cannot grow in an auto-height shell -- there is no fixed total to divide -- so
|
||||
the freed space pooled under the bar as white nothing. In page-scroll the collapsed map takes
|
||||
the SAME shell budget the open state uses, minus only the bar: the one-owner derivation again,
|
||||
so collapse/expand and both drag handles all move the same quantity instead of fighting.
|
||||
Both the ROW and the ELEMENT get the height, for the same reason they share --rd-map-h when
|
||||
open: two owners of one height is the bug this file keeps re-learning. */
|
||||
.rd-shell.rd-page-scroll.rd-table-collapsed{
|
||||
grid-template-rows: var(--rd-topbar-h,38px) auto var(--rd-stats-h,22px)
|
||||
calc(var(--rd-shell-h, 542px) - var(--table-bar-h,30px)) var(--table-bar-h,30px); }
|
||||
.rd-shell.rd-page-scroll.rd-table-collapsed > [data-region="map"]{
|
||||
height:calc(var(--rd-shell-h, 542px) - var(--table-bar-h,30px)); }
|
||||
.rd-shell.rd-page-scroll > [data-region="map"]{ height:var(--rd-map-h, 300px); overflow:hidden; }
|
||||
.rd-shell.rd-page-scroll > [data-region="table"]{ overflow:visible; }
|
||||
.rd-shell.rd-page-scroll .rd-table-wrap{ height:auto; }
|
||||
/* ★ THE TABLE KEEPS AN INTERNAL SCROLL, BUT A GENEROUS ONE — and that is the whole fix.
|
||||
|
|
@ -456,7 +523,7 @@ useShadowStyles(`
|
|||
GOAL CONTEXT: "we are trying to RECLAIM HEIGHT." 7 rows is 182px against today's 300px keyhole,
|
||||
so this alone returns 118px of the ~147px available. */
|
||||
.rd-shell.rd-page-scroll .rd-grid{
|
||||
max-height:var(--rd-table-max-h, calc(var(--rd-table-rows, 7) * var(--rd-table-row-h, 26px))); }
|
||||
max-height:var(--rd-table-max-h, calc(var(--rd-shell-h, 542px) - var(--rd-map-h, 300px) - var(--rd-table-bar-h, 32px) - var(--rd-table-foot-h, 28px))); }
|
||||
/* ── FACETS/RAIL COLUMNS BOUNDED IN PAGE-SCROLL MODE (BUG-DASHBOARD-BAND-GROWS-ENORMOUS) ──────
|
||||
THE ROOT CAUSE, one level up from the table rule above. .rd-facets/.rd-rail are authored
|
||||
height:100%, which is correct in the FIXED-VIEWPORT grid (rows sum to a real, bounded
|
||||
|
|
@ -533,7 +600,14 @@ useShadowStyles(`
|
|||
NUMBER the cap computes was wrong, not the need for a cap at all. */
|
||||
.rd-shell.rd-page-scroll > [data-region="facets"],
|
||||
.rd-shell.rd-page-scroll > [data-region="rail"]{ min-height:0; overflow:hidden;
|
||||
max-height:calc(var(--rd-shell-h, 80svh) + 1px); }
|
||||
/* ★ FALLBACK UNIFIED AT 502px (was 80svh = 720px). Two rules read --rd-shell-h and their
|
||||
FALLBACKS disagreed: the table keyhole fell back to 502px (seven rows) while this cap fell
|
||||
back to 720px. Nothing sets the var at rest, so the sidebars believed the shell was 119px
|
||||
taller than the table did -- the spanning columns stretched the auto table row to fit, and
|
||||
the surplus painted as the grey band the Driver reported between/under map and table, and
|
||||
made a divider drag look broken (the row would not follow, the slack just moved).
|
||||
ONE QUANTITY, ONE OWNER applies to fallbacks too: same var, same fallback, everywhere. */
|
||||
max-height:calc(var(--rd-shell-h, 542px) + 1px); }
|
||||
|
||||
/* ★ THE ELEMENT MUST SIZE TO ITS CONTENT IN PAGE-SCROLL (Driver 2026-09-23: "massive grey gap
|
||||
between map and table and the map overflows into the people-group grid page — the dashboard
|
||||
|
|
@ -561,7 +635,7 @@ useShadowStyles(`
|
|||
UNPROVEN because no host has yet been measured before-and-after through this path. */
|
||||
:host([data-page-scroll="true"]){ height:auto; min-height:0; }
|
||||
|
||||
.rd-shell.rd-narrow{ grid-template-columns:1fr; grid-template-rows:var(--rd-topbar-h,44px) auto var(--rd-stats-h,26px) minmax(0,1fr) 46px;
|
||||
.rd-shell.rd-narrow{ grid-template-columns:1fr; grid-template-rows:var(--rd-topbar-h,38px) auto var(--rd-stats-h,22px) minmax(0,1fr) 46px;
|
||||
grid-template-areas:"topbar" "filters" "stats" "body" "seg"; }
|
||||
.rd-shell.rd-narrow > [data-region]{ grid-area:body; min-width:0; min-height:0; }
|
||||
.rd-shell.rd-narrow > [data-region="topbar"]{ grid-area:topbar; }
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ useShadowStyles(`
|
|||
restore white-space:nowrap + ellipsis here, and do not re-cap this to the donut's height:
|
||||
that cap is precisely what forced the unreadable ellipsis the Driver rejected. */
|
||||
.rd-legend{ display:flex; flex-direction:column; gap:2px; font-size:10.5px; color:var(--dx-text, #3b463d); max-height:120px;
|
||||
overflow-y:auto; overflow-x:auto; flex:1 1 auto; min-width:0;
|
||||
overflow-y:auto; overflow-x:hidden; flex:1 1 auto; min-width:0;
|
||||
scrollbar-width:thin; scrollbar-color:var(--dx-scroll-thumb, #c4c8c0) transparent; }
|
||||
.rd-legend::-webkit-scrollbar{ width:6px; height:6px; }
|
||||
.rd-legend::-webkit-scrollbar-track{ background:transparent; }
|
||||
|
|
@ -200,11 +200,46 @@ useShadowStyles(`
|
|||
now two lines tall, and centring would float the swatch and the count in the middle of that
|
||||
block, detached from the first line of text they label. The count is top-aligned with a 1px
|
||||
nudge (.rd-li b) so it reads as belonging to the row's first line. */
|
||||
.rd-li{ display:flex; align-items:flex-start; gap:5px; cursor:pointer; padding:1px 2px; border-radius:3px; }
|
||||
/* ★ THE CONVENTIONAL LEGEND ROW, on the Driver's final ruling (2026-09-23): "I am not
|
||||
impressed with that design -- use conventional designs only", after rejecting both the
|
||||
scroll-to-read variant ("I cannot scroll right") and the frozen column's painted backing
|
||||
("super white background, it looks odd; it was to be invisible"). This is the pattern every
|
||||
chart library ships: swatch, label that ELLIPSIZES at the available width, count right-
|
||||
aligned in a fixed-width slot. No horizontal scroll, no sticky, no background -- the number
|
||||
needs no backing because nothing can ever slide beneath it. The full label lives in the
|
||||
row's existing :title tooltip. Rows stretch to the container, so every count shares one
|
||||
right edge by ordinary flex geometry. */
|
||||
.rd-li{ display:flex; align-items:center; gap:5px; cursor:pointer; padding:1px 2px; border-radius:3px; }
|
||||
.rd-li > i{ flex:0 0 auto; }
|
||||
.rd-li .rd-lt{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } /* max-content: the row is as wide as its label, so the scrollport has something to scroll */
|
||||
.rd-li:hover{ background:var(--dx-surface, #f3f3f1); } .rd-li.on{ background:#fff4ea; font-weight:700; color:var(--rd-ink, #3b463d); }
|
||||
/* margin-top nudges keep the swatch and the count optically on the label's FIRST line now that
|
||||
a row can be two lines tall. */
|
||||
.rd-li i{ width:9px; height:9px; border-radius:2px; flex:0 0 auto; margin-top:3px; } .rd-li b{ margin-left:auto; color:var(--rd-muted, #6d766e); font-variant-numeric:tabular-nums; flex:0 0 auto; padding-left:4px; margin-top:1px; }
|
||||
.rd-li i{ width:9px; height:9px; border-radius:2px; flex:0 0 auto; margin-top:3px; }
|
||||
/* ★ THE COUNT IS A FROZEN COLUMN (Driver 2026-09-23): "the number column was supposed to be
|
||||
FROZEN with the z-index topped with a WHITE BACKGROUND so that the text doesn't underlap it, so
|
||||
that whenever you scroll to the right you can read the text but the numbers stay visible."
|
||||
This is the other half of the horizontal-scroll change and shipping without it was incomplete:
|
||||
nowrap + overflow-x:auto made the long label reachable, but the label then slides UNDER the
|
||||
count and the two overlap mid-scroll. A scrollable row needs a pinned column, or the thing you
|
||||
scrolled to read collides with the thing you were reading it against.
|
||||
position:sticky right:0 pins it to the SCROLLPORT's right edge, so it holds while the label
|
||||
travels beneath it. The opaque background is load-bearing, not decoration — without it the
|
||||
label shows THROUGH the number and both become unreadable, which is worse than clipping.
|
||||
Background is --rd-panel (the card's own surface) so the pin matches its host rather than
|
||||
assuming white; the card is #fff today but a host that re-skins the panel should not get a
|
||||
white rectangle floating on a tinted card. */
|
||||
/* ★ THE COUNT COLUMN IS UNIFORM, RIGHT-JUSTIFIED AND IMMOVABLE (Driver 2026-09-23: "set the
|
||||
number column to be justified right, same width, uniform, immovable... so it is easier to
|
||||
use -- and the same goes for the region chart"). One fixed width for every row means the
|
||||
digits align into a scannable column instead of each number sitting wherever its label ended;
|
||||
right-justified plus the existing tabular-nums makes magnitudes comparable at a glance (541
|
||||
visibly longer than 72). 3.4em clears the widest live value (1,309 in the region chart) with
|
||||
a little air. sticky right:0 (from the frozen-column fix) is what makes it immovable while the
|
||||
label scrolls beneath. Both donut legends share .rd-li, so religion and region get this from
|
||||
the one rule -- which is exactly why it lives here and not per-chart. */
|
||||
.rd-li b{ flex:0 0 auto; min-width:3.4em; text-align:right;
|
||||
color:var(--rd-muted, #6d766e); font-variant-numeric:tabular-nums; }
|
||||
/* The label is the ONLY item allowed to shrink. It used to be a bare text node, which in a
|
||||
nowrap flex row cannot shrink at all: the row grew past .rd-legend (overflow-x:hidden) and
|
||||
carried the VALUE out of sight — "Ethnoreligion - Chinese Folk Religion" rendered with no
|
||||
|
|
|
|||
|
|
@ -469,7 +469,42 @@ function toggleDock() { collapsed.value = !collapsed.value; bus && bus.emit('tog
|
|||
* actually delivered to a host page.
|
||||
*/
|
||||
const isFull = ref(false)
|
||||
const wrapEl = ref(null)
|
||||
// ★ NATIVE FULLSCREEN, NOT A FIXED OVERLAY (Driver 2026-09-23: "there is no way to exit full
|
||||
// screen or export csv when you enter full screen"). The bar with BOTH buttons is inside this
|
||||
// wrap, so structurally they should have been visible -- but position:fixed inside a shadow root
|
||||
// resolves against any transformed/contained ANCESTOR, not the viewport, and in the cap-5 shell
|
||||
// that pinned the "fullscreen" wrap to a pane whose top sat above the visible area: the bar (and
|
||||
// its Exit + Export CSV) rendered off-screen. The leaf host has no such ancestor, which is why
|
||||
// every leaf measurement passed while the shell was broken -- the two-host rule again.
|
||||
// element.requestFullscreen() is the wheel the platform ships: the browser guarantees viewport
|
||||
// coverage regardless of ancestor transforms, supplies ESC natively, and fires fullscreenchange
|
||||
// so our button state can follow EVERY exit path (ESC, F11, another element taking fullscreen)
|
||||
// instead of only our own toggle. The .is-full class stays as the styling hook and as the
|
||||
// fallback when the API is unavailable (iframes without allowfullscreen).
|
||||
function _syncFull() {
|
||||
// ★ SHADOW BOUNDARY: document.fullscreenElement retargets to the outermost shadow HOST, and
|
||||
// host.contains(innerNode) is false across the boundary -- so the naive check reported "not
|
||||
// fullscreen" while the browser was visibly fullscreen (measured: fullscreenElement had the
|
||||
// custom element's empty className, and the button state never flipped). The real element is on
|
||||
// the wrap's OWN root: getRootNode().fullscreenElement pierces exactly one level, which is where
|
||||
// this wrap lives.
|
||||
const w = wrapEl.value
|
||||
const root = w && w.getRootNode ? w.getRootNode() : document
|
||||
const fsEl = (root && root.fullscreenElement) || document.fullscreenElement
|
||||
isFull.value = !!(fsEl && w && (fsEl === w || (fsEl.contains && fsEl.contains(w))))
|
||||
}
|
||||
if (typeof document !== 'undefined') document.addEventListener('fullscreenchange', _syncFull)
|
||||
onBeforeUnmount(() => { if (typeof document !== 'undefined') document.removeEventListener('fullscreenchange', _syncFull) })
|
||||
function toggleFull() {
|
||||
const el = wrapEl.value
|
||||
if (el && el.requestFullscreen) {
|
||||
if (document.fullscreenElement) { document.exitFullscreen().catch(() => {}) }
|
||||
else { el.requestFullscreen().then(_syncFull).catch(() => { isFull.value = true }) } // fallback: the old overlay
|
||||
// isFull follows via fullscreenchange; the collapsed-expand below still applies either way
|
||||
if (!isFull.value && collapsed.value) { collapsed.value = false; bus && bus.emit('toggleTable', false) }
|
||||
return
|
||||
}
|
||||
isFull.value = !isFull.value
|
||||
// Entering while collapsed would show a fullscreen HEADER BAR and nothing else. The user
|
||||
// asked for the table, so expand it — and tell the shell, which owns the grid row, or the
|
||||
|
|
@ -480,7 +515,14 @@ function toggleFull() {
|
|||
}
|
||||
// Esc is the fullscreen contract every user already knows; without it a pseudo-fullscreen
|
||||
// overlay is a trap, since there is no browser-supplied exit the way the native API gives one.
|
||||
function onKeydown(e) { if (e.key === 'Escape' && isFull.value) { isFull.value = false } }
|
||||
function onKeydown(e) {
|
||||
if (e.key !== 'Escape') return
|
||||
// Native fullscreen: delegate to the API so EVERY consumer of fullscreenchange stays in sync.
|
||||
// (A real user's ESC is handled by the browser itself; this path serves tests and the overlay
|
||||
// fallback, where no browser exit exists and a trapped user was the original defect.)
|
||||
if (document.fullscreenElement) { document.exitFullscreen().catch(() => {}) ; return }
|
||||
if (isFull.value) isFull.value = false
|
||||
}
|
||||
|
||||
// ── footer aggregation ──
|
||||
const sumPop = computed(() => filtered.value.reduce((s, r) => s + (num(r.population) || 0), 0))
|
||||
|
|
@ -560,6 +602,24 @@ useShadowStyles(`
|
|||
pointer-events:none; color:var(--dx-muted, #6d766e); font-size:12px; }
|
||||
/* Hidden where the clusters would collide with it — at narrow widths there is no free middle. */
|
||||
@media (max-width: 1100px){ .rd-bar-hint{ display:none; } }
|
||||
/* ★ THE BAR IS A SELECTION TOOLBAR WHILE A SELECTION EXISTS (the 1280px flood, measured: with
|
||||
one row selected the bar wrapped to 43px and pushed a control out of it -- the Driver's
|
||||
"flooded with buttons that overflow"). The centred hint and the selection cluster cannot both
|
||||
fit a 666px middle column, and the hint is the one carrying redundant information there: its
|
||||
count already lives in the selection pill. It yields exactly while a selection exists and
|
||||
returns the moment the selection clears -- state-scoped, so the resting bar is unchanged.
|
||||
(The 1100px media rule above is the same idea keyed on width; this one is keyed on state.) */
|
||||
.rd-table-wrap:not([data-sel-count="0"]) .rd-bar-hint{ display:none; }
|
||||
/* ★ MEASURED AT 1280 (the Driver's flood, second cause): hiding the hint was not enough -- the
|
||||
in-flow title and count are separate children, and squeezed to 48px the title wrapped
|
||||
INTERNALLY ("People"/"Groups" on two lines), growing the bar to 43px and tumbling the spacer.
|
||||
While a selection exists they say nothing the bar does not already say -- the pill carries the
|
||||
selection count and the composite's own section header above the table already reads "People
|
||||
Groups" -- so they yield with the hint and return on clear. After: every child on one 32px
|
||||
line at 1280, zero overflow. */
|
||||
.rd-table-wrap:not([data-sel-count="0"]) .ttl,
|
||||
.rd-table-wrap:not([data-sel-count="0"]) .ct{ display:none; }
|
||||
.ttl{ white-space:nowrap; } /* and the title may never two-line itself, selection or not */
|
||||
/* Export CSV — lives on THIS bar now (moved from the dashboard topbar). Material-light DX palette:
|
||||
primary #92b195 surface, deep ink label, border #d1d4cd. Stays visible while collapsed. */
|
||||
.rd-table-bar .rd-csv{ display:inline-flex; align-items:center; gap:6px; flex:0 0 auto; cursor:pointer; user-select:none;
|
||||
|
|
@ -675,6 +735,8 @@ useShadowStyles(`
|
|||
position:fixed takes the wrapper out of the shell's grid row without moving it in the TREE,
|
||||
so every inherited token and every ancestor rule still applies and we override only what we
|
||||
mean to. z-index sits just under the max a browser honours, above the map's own controls. */
|
||||
.rd-table-wrap:fullscreen{ background:var(--dx-elevated,#fff); overflow:auto; }
|
||||
.rd-table-wrap:fullscreen .rd-grid{ max-height:none !important; }
|
||||
.rd-table-wrap.is-full{ position:fixed; top:0; left:0; right:0; bottom:0; z-index:2147483646;
|
||||
height:100dvh; width:100vw; box-shadow:0 0 0 100vmax rgba(59,70,61,.22); }
|
||||
/* ★ THE OVERRIDE THAT ACTUALLY DOES THE WORK. The shell caps this element at seven rows via
|
||||
|
|
@ -708,7 +770,7 @@ useShadowStyles(`
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rd-table-wrap" :class="{ 'is-full': isFull }" :data-sel-count="selCount"
|
||||
<div ref="wrapEl" class="rd-table-wrap" :class="{ 'is-full': isFull }" :data-sel-count="selCount"
|
||||
:data-fullscreen="isFull ? '1' : '0'">
|
||||
<!-- Header bar — ALWAYS rendered, collapsed or not. The count + Export CSV are the two
|
||||
controls the Driver asked to keep docked under the map when the grid is hidden. -->
|
||||
|
|
@ -718,9 +780,18 @@ useShadowStyles(`
|
|||
<span class="ttl">People Groups</span>
|
||||
<span class="ct" data-table-count>{{ fmt(rowCount) }} {{ rowCount === 1 ? 'group' : 'groups' }}</span>
|
||||
<!-- role=status so assistive tech announces the count as the selection changes. -->
|
||||
<!-- ★ NO CLEAR BUTTON IN THIS BAR (Driver 2026-09-23, on the selection flood: "it does not
|
||||
need to give me a Clear Selected button in the header because the FOOTER of the page
|
||||
already gives me that"). One action, one home; this pill is a status readout only.
|
||||
★ VERIFIED WHAT ACTUALLY CLEARS, rather than repeating the premise: no literal footer
|
||||
clear exists in THIS component -- the surviving clear paths are the thead master
|
||||
checkbox (un-checking it deselects all rows) and the detail drawer's close
|
||||
(DashboardDetailDrawer.vue:104 calls bus.clearSelection()). Both remain, so removing
|
||||
the header x removes a DUPLICATE, not the capability. clearSel() stays defined for the
|
||||
bus and any future caller. This also deletes the one control in the cluster that read
|
||||
as "a button that doesn't make sense" -- a bare x with no label. -->
|
||||
<span v-if="selCount" class="rd-pill" data-sel-pill role="status">
|
||||
<span>{{ fmt(selCount) }} selected</span>
|
||||
<button title="Clear selection" aria-label="Clear selection" @click="clearSel">✕</button>
|
||||
</span>
|
||||
<!-- Selection VIEWS — separated from the pill because they mutate the VIEW, not the
|
||||
selection. Only meaningful once something is selected, so they appear with it. -->
|
||||
|
|
|
|||
|
|
@ -480,7 +480,15 @@ useShadowStyles(`
|
|||
.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; }
|
||||
.fr-sec{ border-bottom:1px solid var(--rd-line, #d1d4cd); }
|
||||
.fr-sech{ display:flex; align-items:center; gap:6px; padding:5px 12px; cursor:pointer; user-select:none;
|
||||
/* ★ STICKY SECTION HEADERS, GITHUB-MARKDOWN STYLE (Driver 2026-09-23: "when I am scrolling
|
||||
down it needs to keep the section headers visible until you scroll into the next section --
|
||||
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
|
||||
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
|
||||
over transparent paints the rows through the label. z-index keeps it above row hover states. */
|
||||
.fr-sech{ position:sticky; top:0; z-index:2; background:var(--rd-panel,#fff);
|
||||
display:flex; align-items:center; gap:6px; padding:5px 12px; cursor:pointer; user-select:none;
|
||||
font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.5px; color:var(--rd-muted, #6d766e); }
|
||||
.fr-sech:hover{ background:var(--rd-bg, #f3f3f1); }
|
||||
.fr-sech .chev{ margin-left:auto; transition:transform .15s; font-size:10px; color:var(--dx-muted, #6d766e); }
|
||||
|
|
@ -680,11 +688,15 @@ useShadowStyles(`
|
|||
<template>
|
||||
<aside class="rd-facets" data-region="facets">
|
||||
<div class="fr-head">
|
||||
<!-- "Tree Legend" — the Driver's naming ruling: the word "semantic" is dropped from the
|
||||
<!-- "Vertical Legend" — the Driver's naming ruling (2026-09-23): "it shouldn't say tree
|
||||
legend... rename the component to be called Vertical... use the term nesting or
|
||||
folders". The visible title says VERTICAL; the drill-down vocabulary in code and
|
||||
comments is NESTING/FOLDERS, not tree/semantic. Earlier ruling, kept: -->
|
||||
<!-- (prior note) the word "semantic" is dropped from the
|
||||
user-facing label (the component/docs still describe the semantic-tree MODEL).
|
||||
The four stacked trees below it are People · Place · Language · Religion, in that
|
||||
order — the four axes a researcher navigates to find who has gospel resources. -->
|
||||
<div class="fr-title">Tree Legend</div>
|
||||
<div class="fr-title">Vertical Legend</div>
|
||||
<div class="fr-scope" data-scope-count><b>{{ scopeCount.toLocaleString('en-US') }}</b> groups in scope</div>
|
||||
</div>
|
||||
<div class="fr-body" ref="railEl" @click="onRowClick" @keydown="onRowKey">
|
||||
|
|
|
|||
|
|
@ -29,11 +29,17 @@ import { buildSearchAccessors, makeSearchMatcher } from '../../composables/useDa
|
|||
// dot a prayer filter leaves on the map can never disagree with the level the filter selected —
|
||||
// one threshold, one owner. (The dashboard facet already reuses getPrayerLevel for the same reason.)
|
||||
import { applyColor as prayerColorExpr } from '@map/colors/prayer-progress.js'
|
||||
import { resolveAssetsBase } from '@map/utils/embedBase.js'
|
||||
|
||||
const dash = inject('dashboardData', null)
|
||||
const bus = inject('selectionBus', null)
|
||||
const mapboxToken = inject('mapboxToken', ref(''))
|
||||
const cfg = inject('dashboardConfig', {})
|
||||
// The host's parsed profile-config, used ONLY to resolve the assets base for the keyless country
|
||||
// boundaries on the MapLibre path (see addLayers). Same channel every other plugin already reads;
|
||||
// resolveAssetsBase degrades correctly when it is null, so a host that provides nothing still
|
||||
// resolves the tree from this bundle's own script URL.
|
||||
const profileConfig = inject('profileConfig', ref(null))
|
||||
const filterSet = bus?.filterSet || {}
|
||||
|
||||
const FACET_ACC = Object.fromEntries(Object.entries(cfg.facets || {}).map(([k, f]) => [k, f.get]))
|
||||
|
|
@ -86,11 +92,30 @@ const mapContainer = ref(null)
|
|||
// ★ THE TOKEN IS NOT DROPPED WHEN provider='maplibre'. accessToken stays wired: MapLibre ignores
|
||||
// it, and keeping it means flipping the prop back to 'mapbox' is a one-word change with nothing
|
||||
// else to restore. That is what "hot-swappable" has to mean to be worth anything.
|
||||
const mapProvider = computed(() => (cfg.provider === 'maplibre' ? 'maplibre' : 'mapbox'))
|
||||
// ★ READ BOTH LEVELS, for the same reason `style` below does. This originally read ONLY
|
||||
// `cfg.provider`, while every other map setting (mapStyle, bins, geoFacet, colorRamp) lives under
|
||||
// `cfg.map`. A host that put `provider` where the rest of its map config already sits got NO
|
||||
// error and NO swap — the map silently stayed on Mapbox and the config looked correct. That is
|
||||
// the exact failure the note on `style` below names: a setting silently ignored because it sat
|
||||
// one level off is worse than no setting. `cfg.map.provider` is the spelling to prefer; the
|
||||
// top-level one is kept working so nothing already using it breaks.
|
||||
const mapProvider = computed(() => {
|
||||
const p = cfg.map?.provider || cfg.provider
|
||||
return p === 'maplibre' ? 'maplibre' : 'mapbox'
|
||||
})
|
||||
// Derived once so the source/layer branches in addLayers read the SAME decision the engine was
|
||||
// loaded with — a second inline `cfg.provider === …` test is how the two drift apart.
|
||||
const isMaplibreProvider = computed(() => mapProvider.value === 'maplibre')
|
||||
|
||||
const { map, isMapReady, initializeMap, destroy } = useMapInstance({
|
||||
containerRef: mapContainer, accessToken: unref(mapboxToken) || (mapboxToken && mapboxToken.value) || '',
|
||||
center: [28, 18], zoom: 1.45, minZoom: 1,
|
||||
// ★ ZOOMED OUT BY DEFAULT, WITH REAL ZOOM-OUT RANGE (Driver 2026-09-23: "I don't want the map
|
||||
// zoomed in by default but zoomed out... the edges of the map need to be truly around the edges
|
||||
// so you have more zoom control and zoom function distance"). zoom 1.45 -> 1.1 opens on the
|
||||
// whole sphere; minZoom 1 -> 0.5 plus skipWorldFloor gives the zoom-out gesture somewhere to
|
||||
// go. The floor being skipped is the mercator "no duplicate world" rule, which is meaningless
|
||||
// on a globe -- a sphere HAS no second world to show. Flat mercator keeps the floor.
|
||||
center: [28, 18], zoom: 1.1, minZoom: 0.5, skipWorldFloor: true,
|
||||
provider: mapProvider.value,
|
||||
// A named style resolves per-provider via the registry; leaving it unset keeps each engine's own
|
||||
// default, which is what shipped before this change.
|
||||
|
|
@ -464,7 +489,14 @@ function setLiveResize(on) {
|
|||
const BIN_COLORS = (() => {
|
||||
const RAMP_PRESETS = {
|
||||
'heartbeat-red': ['#f4cec4', '#ecaea1', '#e08678', '#ce544b', '#b10010'],
|
||||
greyscale: ['#e8e8e8', '#a9a9a9', '#6f6f6f', '#3a3a3a', '#060606'],
|
||||
// ★ BIN 1 DARKENED #e8e8e8 -> #c9c9c9 (Driver 2026-09-23: "bangladesh is not showing up as a
|
||||
// polygon"). It WAS rendering -- queryRenderedFeatures returned the BGD polygon painted
|
||||
// #e8e8e8 -- but against the #f3f3f1 no-data surface that is a 1.06 contrast ratio at 0.78
|
||||
// opacity: a 1-4-group country was pixel-identical to an empty one. The ramp's first step
|
||||
// must be separated from NO-DATA at least as clearly as from bin 2, or the lowest bin
|
||||
// silently reads as "not in the data" (ColorBrewer sequential ramps assume no competing
|
||||
// no-data fill on the same canvas; when there is one, start the ramp a step darker).
|
||||
greyscale: ['#c9c9c9', '#a9a9a9', '#6f6f6f', '#3a3a3a', '#060606'],
|
||||
}
|
||||
return RAMP_PRESETS[cfg.map?.colorRamp] || RAMP_PRESETS['heartbeat-red']
|
||||
})()
|
||||
|
|
@ -749,9 +781,54 @@ function refreshSelected() {
|
|||
function addLayers() {
|
||||
const m = map.value; if (!m || added) return
|
||||
added = true
|
||||
if (!m.getSource('rd-countries')) m.addSource('rd-countries', { type: 'vector', url: 'mapbox://mapbox.country-boundaries-v1' })
|
||||
// ── COUNTRY POLYGONS: the choropleth's source, PER PROVIDER ─────────────────────────────────
|
||||
// ★ THIS IS WHY THE MapLibre SWAP IS NOT ONE CONFIG LINE. The choropleth read from
|
||||
// `mapbox://mapbox.country-boundaries-v1`, a Mapbox-hosted VECTOR TILESET. MapLibre cannot
|
||||
// fetch a mapbox:// URL at all — it throws
|
||||
// Fetch API cannot load mapbox://mapbox.country-boundaries-v1. URL scheme "mapbox" is not supported
|
||||
// and then renders the basemap perfectly with NO choropleth on it. MEASURED, not predicted:
|
||||
// the first MapLibre screenshot showed a correct black-and-blue galaxy map on which every
|
||||
// landmass was uniform black while the legend still advertised five populated bins. It looked
|
||||
// like a deliberate dark style. The primary data layer was simply missing.
|
||||
//
|
||||
// So the swap needs a KEYLESS equivalent, and the tree already ships one:
|
||||
// assets/boundaries/world-adm0.geojson (162 ADM0 features), already staged by generate-tree
|
||||
// and already consumed by three other plugins (upg-100-list, connectivity-map,
|
||||
// resources-map) through the same resolveAssetsBase helper. No new asset, no new dependency.
|
||||
//
|
||||
// ★ THE KEY BRIDGE, AND WHY IT IS DONE HERE RATHER THAN IN THE EXPRESSIONS. The tileset names
|
||||
// the ISO field `iso_3166_1_alpha_3`; the geojson names it `iso3`. Six separate places read
|
||||
// that field — buildPaint's fill-color AND fill-opacity match expressions, the click handler,
|
||||
// the hover handler, and the queryRenderedFeatures path. Branching each of them would be six
|
||||
// chances to miss one, and a missed one fails silently (a `match` on an absent property just
|
||||
// falls through to its default, i.e. renders as no-data). Instead the property is normalized
|
||||
// ONCE, as the data is loaded, so every downstream expression keeps reading the single name it
|
||||
// always read and the Mapbox path stays byte-for-byte unchanged.
|
||||
if (!m.getSource('rd-countries')) {
|
||||
if (isMaplibreProvider.value) {
|
||||
// Add EMPTY first, then setData after the fetch: addLayer below must run synchronously to
|
||||
// keep this layer underneath the pins (layer order is insertion order). An awaited fetch
|
||||
// here would put the choropleth on TOP of every bubble.
|
||||
m.addSource('rd-countries', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } })
|
||||
fetch(`${resolveAssetsBase(profileConfig.value)}/boundaries/world-adm0.geojson`)
|
||||
.then((r) => r.json())
|
||||
.then((gj) => {
|
||||
for (const f of gj.features || []) {
|
||||
if (f.properties && f.properties.iso3) f.properties.iso_3166_1_alpha_3 = f.properties.iso3
|
||||
}
|
||||
const src = map.value && map.value.getSource('rd-countries')
|
||||
if (src) src.setData(gj)
|
||||
})
|
||||
.catch((e) => console.warn('[rdash] country boundaries failed to load', e))
|
||||
} else {
|
||||
m.addSource('rd-countries', { type: 'vector', url: 'mapbox://mapbox.country-boundaries-v1' })
|
||||
}
|
||||
}
|
||||
const { fillColor, fillOpacity } = buildPaint()
|
||||
m.addLayer({ id: 'rd-choropleth', type: 'fill', source: 'rd-countries', 'source-layer': 'country_boundaries',
|
||||
m.addLayer({ id: 'rd-choropleth', type: 'fill', source: 'rd-countries',
|
||||
// 'source-layer' applies to VECTOR TILE sources only. On a geojson source MapLibre rejects
|
||||
// the layer outright ("does not need a source-layer"), so it is set for Mapbox alone.
|
||||
...(isMaplibreProvider.value ? {} : { 'source-layer': 'country_boundaries' }),
|
||||
// COUNTRY BORDERS. The outline was '#ffffff', which was legible only because the fill
|
||||
// underneath ran to black. On a pale ramp a white border measures 1.11–1.84 — it would
|
||||
// vanish and the map would read as one undifferentiated wash. The Material Light MUTED
|
||||
|
|
@ -959,6 +1036,23 @@ function addLayers() {
|
|||
}
|
||||
} catch (_) { /* verify hook */ }
|
||||
applyProjection()
|
||||
// ★ THE GALAXY, THE WAY MAPBOX DOES IT (Driver: "find out how Mapbox does the galaxy stars and
|
||||
// see if we can use that"). Mapbox GL renders space and the starfield ENGINE-SIDE via the fog
|
||||
// API -- documented at docs.mapbox.com/mapbox-gl-js/guides/globe/ ("stars and atmosphere are
|
||||
// customized with the fog property"). These are their documented example values with one
|
||||
// change: space-color is pure black on the Driver's instruction ("not grey"). star-intensity
|
||||
// 0..1 is the engine's own star layer -- no canvas, no custom code, the actual Mapbox stars.
|
||||
// Guarded: MapLibre has no setFog (its equivalent is the sky spec), so on that provider this is
|
||||
// a silent no-op and the plain black container behind the transparent sphere carries the void.
|
||||
try {
|
||||
if (typeof m.setFog === 'function') m.setFog({
|
||||
'color': 'rgb(186, 210, 235)', // lower-atmosphere haze at the horizon
|
||||
'high-color': 'rgb(36, 92, 223)', // upper atmosphere
|
||||
'horizon-blend': 0.02,
|
||||
'space-color': '#000000', // the void — black, not grey
|
||||
'star-intensity': 0.6,
|
||||
})
|
||||
} catch (_) { /* an engine without fog keeps the container background instead */ }
|
||||
// selection pen: rebuild the highlight source whenever the selectionSet changes (toggle on/off).
|
||||
if (bus) bus.on('select', refreshSelected)
|
||||
refreshSelected()
|
||||
|
|
@ -1246,7 +1340,21 @@ function syncAttribClearance() {
|
|||
|
||||
// ── RDASH-ENHANCE: GLOBE (default) <-> MERCATOR projection toggle ──
|
||||
const projection = ref('globe')
|
||||
function applyProjection() { const m = map.value; if (m) { try { m.setProjection(projection.value) } catch (_) {} } }
|
||||
// ★ THE TWO ENGINES DISAGREE ON THIS ONE CALL, AND THE CATCH HID IT (Driver 2026-09-23: "the map
|
||||
// style is not a globe"). Mapbox GL takes setProjection('globe') -- a bare string. MapLibre v5 takes
|
||||
// setProjection({ type: 'globe' }) -- an object. Passing a string to MapLibre throws, the empty
|
||||
// catch below swallowed it, and the map silently fell back to flat mercator with NO console error.
|
||||
// That is why this appeared the moment the dashboard moved to provider:'maplibre': the default was
|
||||
// still 'globe' and the request simply stopped arriving.
|
||||
// Try the object form first (MapLibre, and harmless to attempt), then the string (Mapbox). The catch
|
||||
// stays because a provider without ANY projection support must degrade rather than break the panel --
|
||||
// but it now catches a genuine unsupported-engine case instead of masking a shape mismatch.
|
||||
function applyProjection() {
|
||||
const m = map.value
|
||||
if (!m || typeof m.setProjection !== 'function') return
|
||||
try { m.setProjection({ type: projection.value }); return } catch (_) { /* not the object form */ }
|
||||
try { m.setProjection(projection.value) } catch (_) { /* engine has no projection support */ }
|
||||
}
|
||||
function toggleProjection() { projection.value = projection.value === 'globe' ? 'mercator' : 'globe'; applyProjection() }
|
||||
|
||||
// ── RDASH-ENHANCE: fly-to on selection. Any facet brush (tree node / chart slice / region / row)
|
||||
|
|
@ -1428,8 +1536,18 @@ watch(() => JSON.stringify(filterSet), () => { clearTimeout(_flyT); _flyT = setT
|
|||
|
||||
useShadowStyles(`
|
||||
.rd-mapwrap{ position:relative; width:100%; height:100%; min-height:0; background:var(--dx-line, #d1d4cd); }
|
||||
/* ★ THE GALAXY IS THE CONTAINER, NOT THE TILES (Driver 2026-09-23, correcting the first attempt:
|
||||
"I asked for a spherical globe with a black background — the galaxy — with blue oceans like the
|
||||
Mapbox map, and the land was NOT to be black"). On a globe projection the sphere does not fill
|
||||
the canvas; the corners show the CONTAINER's background. So space lives here: a deep radial
|
||||
wash behind the sphere, while the tiles keep their normal daylight colours. Scoped to
|
||||
[data-projection="globe"] so flat mercator keeps the neutral ground — space behind a flat
|
||||
rectangle would read as a mistake. */
|
||||
/* Plain BLACK behind the sphere (Driver: "if not, then just do black background, not grey").
|
||||
On Mapbox the engine paints its own space + stars over the whole canvas via fog, so this only
|
||||
shows on engines without fog support (MapLibre), where black is the honest fallback. */
|
||||
.rd-mapwrap[data-projection="globe"]{ background:#000; }
|
||||
.rd-mapwrap .rd-mapc{ position:absolute; inset:0; }
|
||||
.rd-maptag{ position:absolute; left:8px; bottom:8px; z-index:2; font-size:10px; color:#33415588; background:#ffffffcc; padding:2px 6px; border-radius:4px; }
|
||||
.rd-proj{ position:absolute; right:8px; top:8px; z-index:3; font-size:11px; font-weight:700; cursor:pointer; user-select:none;
|
||||
background:#1a1a2ecc; color:#fff; border:1px solid #ffffff33; border-radius:6px; padding:4px 9px; backdrop-filter:blur(4px); }
|
||||
.rd-proj:hover{ background:#1a1a2e; }
|
||||
|
|
@ -1464,7 +1582,8 @@ useShadowStyles(`
|
|||
The fix RAISES THE LEGEND, and never touches the attribution: nothing here hides, shrinks
|
||||
or restyles .mapboxgl-ctrl-attrib. --rd-attrib-clear is the reserved strip along the bottom
|
||||
edge that the credit line owns; the legend's bottom edge starts above it.
|
||||
Bottom-LEFT was not available — .rd-maptag already holds that corner.
|
||||
Bottom-LEFT was not available when this was written (a caption held that corner; it has since
|
||||
been removed, so the corner is free if a future control needs it).
|
||||
Stays on the right so the legend sits beside the choropleth it explains. */
|
||||
.rd-mapwrap{ --rd-attrib-clear:26px; }
|
||||
.rd-legend-map{ position:absolute; right:8px; bottom:calc(var(--rd-attrib-clear,26px) + 8px); z-index:2;
|
||||
|
|
@ -1525,7 +1644,6 @@ useShadowStyles(`
|
|||
<span class="rd-proj" role="button" tabindex="0" data-proj-toggle @click="toggleProjection">
|
||||
{{ projection === 'globe' ? '🌐 Globe' : '▭ Flat' }}
|
||||
</span>
|
||||
<div class="rd-maptag">UPG choropleth · click a country to filter · click a pin to select</div>
|
||||
<div class="rd-legend-map" data-map-legend aria-label="UPG count legend">
|
||||
<div class="lg-h">UPGs / country</div>
|
||||
<div v-for="b in LEGEND_BINS" :key="b.l" class="lg-row">
|
||||
|
|
|
|||
|
|
@ -165,12 +165,25 @@ const filters = computed(() => dropdownKeys.value.map((key) => ({
|
|||
// ('Needs Prayer'), so passing it through a code resolver can only fail to find it and fall back —
|
||||
// at best a no-op, at worst a blank chip. The value-is-label axes need no resolution by definition.
|
||||
const { labelFor } = useTreeLegendLabels(cfg, inject('dataStore', null), inject('searchSourceId', null))
|
||||
const activeChips = computed(() =>
|
||||
Object.keys(filterSet).map((k) => {
|
||||
const activeChips = computed(() => {
|
||||
const out = Object.keys(filterSet).map((k) => {
|
||||
const v = filterSet[k]
|
||||
return { name: k, label: `${FACETS.value[k]?.label || k}: ${k === PRAYER_KEY ? v : labelFor(k, v)}` }
|
||||
})
|
||||
)
|
||||
// ★ THE TYPED SEARCH IS A FILTER AND MUST LOOK LIKE ONE (Driver 2026-09-23: "I searched Bhutan
|
||||
// and it did not switch the toggle to show the country was selected"). MEASURED: typing narrows
|
||||
// the scope live via bus.searchTerm (2,107 -> 13) while filterSet stays empty, so the data
|
||||
// changed and no control anywhere said why -- invisible state, the same class as the silent
|
||||
// grey pane. The conventional faceted-search answer is that free text is its OWN chip beside
|
||||
// the facet chips, clearable like any of them. It deliberately does NOT masquerade as a Country
|
||||
// selection: the term matches across ALL searchKeys (name, region, language, religion...), so
|
||||
// labelling it a country filter would lie whenever it matches something else.
|
||||
// (First attempt went into DashboardFilterChips.vue -- a component the topbar dedup pass had
|
||||
// already unmounted; THIS band is the documented single source of truth for chips.)
|
||||
const term = (bus?.searchTerm?.value || '').trim()
|
||||
if (term) out.push({ name: '__search__', label: `Search: ${term}` })
|
||||
return out
|
||||
})
|
||||
|
||||
const searchValue = computed(() => bus?.searchTerm?.value || '')
|
||||
|
||||
|
|
@ -178,8 +191,11 @@ const searchValue = computed(() => bus?.searchTerm?.value || '')
|
|||
function onSearch(payload) { bus?.setSearch(payload?.value ?? '') }
|
||||
function onFilterChange(detail) { bus?.setFilter(detail.name, detail.value) }
|
||||
function onFilterClear(detail) { bus?.setFilter(detail.name, null) }
|
||||
function onRemoveFilter(detail) { bus?.setFilter(detail.name, null) }
|
||||
function onClearAll() { bus?.clearFilters() }
|
||||
function onRemoveFilter(detail) {
|
||||
if (detail.name === '__search__') { bus?.setSearch(''); return }
|
||||
bus?.setFilter(detail.name, null)
|
||||
}
|
||||
function onClearAll() { bus?.clearFilters(); bus?.setSearch('') }
|
||||
|
||||
useShadowStyles(`
|
||||
/* header-2 host: the research-filters band sits in the dashboard's dedicated 'filters' grid row.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* A dashboard shell (see plugin-designs/doxa-research-dashboards/research-dashboards.vue) is a CSS
|
||||
* grid whose three flexible tracks are driven by CSS custom properties:
|
||||
* --facet-w (left facet-rail column width) default 296px
|
||||
* --rail-w (right charts-rail column width) default 264px
|
||||
* --rail-w (right charts-rail column width) default 296px — equals --facet-w, so the map column sits centred
|
||||
* --table-h (bottom table row height) default 34% (map above it is the 1fr hero)
|
||||
*
|
||||
* This composable OWNS those three vars: it exposes pointer-based drag handlers (one per edge),
|
||||
|
|
@ -85,7 +85,30 @@ import { ref, unref, onMounted, onBeforeUnmount } from 'vue'
|
|||
// 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;
|
||||
// only DRAGGING behaves differently now (see the SHELL vs. TABLE axis note above).
|
||||
const DEFAULTS = { facetW: 296, railW: 264, tableH: '34%', mapH: 360, shellH: 720 }
|
||||
// ★ shellH 720 -> 602 so the table OPENS at seven data rows (Driver 2026-09-23: "the table needs to
|
||||
// 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
|
||||
// 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
|
||||
// be reached by any gesture. The Driver lost BOTH resizes at once and said so: "I didn't want the
|
||||
// table set to 7. Only the table needs to be resizable. We've set it to where I can't even resize."
|
||||
// Setting the STARTING VALUE instead leaves the derivation intact: the keyhole stays
|
||||
// shell - map - bar - foot, so dragging either handle still moves the table, and seven rows is
|
||||
// merely where it opens.
|
||||
// ★ RECLAIM PASS 2 (Driver 2026-09-23: "it is a priority to reclaim height... so I can see half
|
||||
// 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
|
||||
// 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
|
||||
// shorter map shows MORE world, not less.
|
||||
// shellH 502 -> 542 (Driver 2026-09-23: "the dashboard should be taller by about 40px"). The
|
||||
// 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
|
||||
// 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
|
||||
// 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.
|
||||
const DEFAULTS = { facetW: 296, railW: 296, tableH: '34%', mapH: 300, shellH: 542 }
|
||||
const CLAMPS = { facetW: [160, 420], railW: [200, 520], tableH: [12, 70], mapH: [220, 1400], shellH: [400, 2600] }
|
||||
|
||||
// Mirrors the CSS fallback --rd-table-bar-h (research-dashboard.vue's .rd-table-bar budget calc) —
|
||||
|
|
@ -107,7 +130,15 @@ export function useDashboardLayout(options = {}) {
|
|||
bus = null,
|
||||
isNarrow = false,
|
||||
pageScroll = false,
|
||||
storageKey = 'rd-dashboard-layout',
|
||||
// ★ KEY VERSIONED v2 (2026-09-23). The Driver reported "sidebars are not equally spaced" on
|
||||
// his machine AFTER the symmetric-rail change landed -- because his browser restores a layout
|
||||
// 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
|
||||
// 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.
|
||||
// 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.
|
||||
storageKey = 'rd-dashboard-layout-v4',
|
||||
} = options
|
||||
|
||||
const defaults = { ...DEFAULTS, ...(options.defaults || {}) }
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
* responsive switch, collapsible table dock, segmented mobile view, and the location.hash sync.
|
||||
* Only the SOURCE of the per-dashboard constants changed: from hard-coded → config.
|
||||
*/
|
||||
import { fuzzyMatches } from '@map/components/search/fuzzy.js'
|
||||
import { provide, ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { createSelectionBus } from './useSelectionBus.js'
|
||||
import { useDashboardData } from './useDashboardData.js'
|
||||
|
|
@ -59,12 +60,19 @@ export function buildSearchAccessors(config) {
|
|||
* array from buildSearchAccessors(config). Case-insensitive substring over each accessor's value.
|
||||
*/
|
||||
export function makeSearchMatcher(bus, accessors) {
|
||||
// ★ FUZZY, NOT SUBSTRING (Driver 2026-09-23: "add an n to the end of a country name" must
|
||||
// still find it -- check what 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
|
||||
// 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).
|
||||
// It now delegates to the shared fuzzyMatches() in @map fuzzy.js -- the same matcher the
|
||||
// grid's Fuse uses -- with the same DX-life options, so every search surface agrees.
|
||||
return (row) => {
|
||||
const needle = (bus?.searchTerm?.value || '').trim().toLowerCase()
|
||||
const needle = (bus?.searchTerm?.value || '').trim()
|
||||
if (!needle) return true
|
||||
for (const acc of accessors) {
|
||||
const v = acc(row)
|
||||
if (v != null && String(v).toLowerCase().includes(needle)) return true
|
||||
if (v != null && fuzzyMatches(String(v), needle, { threshold: 0.4, minMatchCharLength: 3 })) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -153,20 +161,61 @@ function useResponsiveShell(bus) {
|
|||
|
||||
/**
|
||||
* Shareable view-state hash URL: serialize filterSet to location.hash; restore on mount.
|
||||
* Extracted VERBATIM from research-dashboard.vue.
|
||||
*
|
||||
* ★ READABLE, CODE-CANONICAL FORMAT (card BUG-SHARE-URL-LEAKS-RAW-CODES). The old hash was
|
||||
* URL-encoded JSON -- #f=%7B%22affinity_bloc%22%3A%22A008%22%7D -- unreadable twice over: JSON
|
||||
* punctuation percent-escaped, and the value a bare API code. A URL is the one part of the
|
||||
* product a user COPIES, PASTES and READS, and for sensitive population data a link you cannot
|
||||
* sanity-check before clicking is a real cost. The format is now
|
||||
*
|
||||
* #f=affinity_bloc:A008*malay-peoples;religion_family:Hinduism
|
||||
*
|
||||
* -- the Stack Overflow /questions/123/slug pattern: the CODE is canonical and the *slug is
|
||||
* DISPLAY-ONLY, appended from labelFor() at share time and STRIPPED on parse. Restoring never
|
||||
* reads the slug, so links stay locale-independent and survive label renames (the card's
|
||||
* explicit constraint: keep the code canonical, do not swap in localized labels).
|
||||
*
|
||||
* `*` is safe as the slug separator because escapeValue() percent-encodes any literal `*` in
|
||||
* the VALUE first -- the separator cannot collide with content. `;` and `:` are legal fragment
|
||||
* chars, so the whole thing stays legible in the address bar. Legacy JSON hashes (old shared
|
||||
* links) are detected by their leading `{` / %7B and still restore.
|
||||
*/
|
||||
function useHashSync(bus) {
|
||||
const slugify = (s) => String(s ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40)
|
||||
// Minimal escaping, maximal legibility: reserve our three delimiters + %, keep spaces as `+`
|
||||
// (the x-www-form-urlencoded convention), leave everything else -- including unicode -- alone.
|
||||
const escapeValue = (s) => String(s).replace(/[%;:*+]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase()).replace(/ /g, '+')
|
||||
const unescapeValue = (s) => { try { return decodeURIComponent(String(s).replace(/\+/g, '%20')) } catch (_) { return String(s).replace(/\+/g, ' ') } }
|
||||
|
||||
function useHashSync(bus, labelFor) {
|
||||
function syncHash() {
|
||||
try {
|
||||
const enc = encodeURIComponent(JSON.stringify(bus.filterSet))
|
||||
if (typeof location !== 'undefined') history.replaceState(null, '', '#f=' + enc)
|
||||
const pairs = Object.entries(bus.filterSet).map(([k, v]) => {
|
||||
const label = typeof labelFor === 'function' ? labelFor(k, v) : null
|
||||
const slug = label && String(label) !== String(v) ? slugify(label) : ''
|
||||
return k + ':' + escapeValue(v) + (slug ? '*' + slug : '')
|
||||
})
|
||||
if (typeof location !== 'undefined') history.replaceState(null, '', '#f=' + pairs.join(';'))
|
||||
} catch (_) { /* sandboxed */ }
|
||||
}
|
||||
watch(() => ({ ...bus.filterSet }), syncHash, { deep: true })
|
||||
onMounted(() => {
|
||||
try {
|
||||
const m = typeof location !== 'undefined' && /[#&]f=([^&]+)/.exec(location.hash)
|
||||
if (m) { const obj = JSON.parse(decodeURIComponent(m[1])); Object.entries(obj).forEach(([k, v]) => bus.setFilter(k, v)) }
|
||||
if (!m) return
|
||||
const raw = m[1]
|
||||
if (raw.startsWith('{') || raw.startsWith('%7B') || raw.startsWith('%7b')) {
|
||||
// legacy JSON hash from an old shared link
|
||||
const obj = JSON.parse(decodeURIComponent(raw))
|
||||
Object.entries(obj).forEach(([k, v]) => bus.setFilter(k, v))
|
||||
return
|
||||
}
|
||||
for (const pair of raw.split(';')) {
|
||||
const i = pair.indexOf(':')
|
||||
if (i < 1) continue
|
||||
const key = pair.slice(0, i)
|
||||
const value = unescapeValue(pair.slice(i + 1).split('*')[0])
|
||||
if (key && value) bus.setFilter(key, value)
|
||||
}
|
||||
} catch (_) { /* ignore bad hash */ }
|
||||
})
|
||||
}
|
||||
|
|
@ -210,9 +259,6 @@ export function useDashboardShell(config) {
|
|||
// responsive flags + collapsible table dock + segmented mobile view.
|
||||
const responsive = useResponsiveShell(bus)
|
||||
|
||||
// shareable view-state hash URL.
|
||||
useHashSync(bus)
|
||||
|
||||
// ── dataStore adapter (GAP B) — DataSourceManager-normalized (DASH-FIX-2) ──
|
||||
// The header's GeocoderComponent bars (DOXA-LIB-3) run useEntitySearch over an
|
||||
// injected `dataStore` to surface local people-group / country / language /
|
||||
|
|
@ -259,6 +305,10 @@ export function useDashboardShell(config) {
|
|||
// here, below `dataStore`, because the resolver reads the normalized features it exposes.
|
||||
// Data columns are unaffected (see useCsvExport.buildCsv).
|
||||
const { labelFor } = useTreeLegendLabels(config, dataStore, searchSourceId)
|
||||
|
||||
// shareable view-state hash URL -- below labelFor on purpose: the hash's display-only slugs
|
||||
// resolve through the SAME code->name resolver the chips and the CSV provenance line use.
|
||||
useHashSync(bus, labelFor)
|
||||
const exporter = useCsvExport({ rowStore: data.rowStore, rows: data.rows, selectionSet: bus.selectionSet, filterSet: bus.filterSet, FACET_ACC, dataOpts: config.data, labelFor })
|
||||
|
||||
provide('selectionBus', bus)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <all-people-groups> custom element. -->
|
||||
<script src="../all-people-groups.js" integrity="sha384-ajyiOLGnsr8AOhrZpwopjFXwKGNk56IbWkBMD+2oYA+DSDjdtATeoF5muZoXhcEF" crossorigin="anonymous"></script>
|
||||
<script src="../all-people-groups.js" integrity="sha384-xK62b4nEd/ei3xDdRNOOCl9YyDRliPUvS8dtna07xFFR1nwPqaCiAHBNuxRtuaIH" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
14
public/doxa-maps/big-picture/big-picture.js
generated
14
public/doxa-maps/big-picture/big-picture.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
|
||||
<script src="../../big-picture.js" integrity="sha384-a9W2VPCO3zJ+1pqWQadGso72rNPTfmZgO4aoGtZN7RDFQpVQbeTvy8JBsGi9VH43" crossorigin="anonymous"></script>
|
||||
<script src="../../big-picture.js" integrity="sha384-MG/HIBfHvBjodOyOgVlzhgK5sZhEg2BI4DGPBc7yUmjGM9CDWI5JMvQh9nHRbJz4" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
|
||||
<script src="../../big-picture.js" integrity="sha384-a9W2VPCO3zJ+1pqWQadGso72rNPTfmZgO4aoGtZN7RDFQpVQbeTvy8JBsGi9VH43" crossorigin="anonymous"></script>
|
||||
<script src="../../big-picture.js" integrity="sha384-MG/HIBfHvBjodOyOgVlzhgK5sZhEg2BI4DGPBc7yUmjGM9CDWI5JMvQh9nHRbJz4" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
|
||||
<script src="../big-picture.js" integrity="sha384-a9W2VPCO3zJ+1pqWQadGso72rNPTfmZgO4aoGtZN7RDFQpVQbeTvy8JBsGi9VH43" crossorigin="anonymous"></script>
|
||||
<script src="../big-picture.js" integrity="sha384-MG/HIBfHvBjodOyOgVlzhgK5sZhEg2BI4DGPBc7yUmjGM9CDWI5JMvQh9nHRbJz4" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-11tC7IzM7DM66gTcAlLSb0bwA7TTkfKYmdrYdiGlBTy9eLVhYCjKgD+LmhZ+qLwO" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-Hz695w/9J1maAa0H9nynW2C6KvE6ltlaaTV9rtkNLDEHnxuI53dXZtiLL7JtVF55" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <country-posters-map> custom element. -->
|
||||
<script src="../country-posters.js" integrity="sha384-11tC7IzM7DM66gTcAlLSb0bwA7TTkfKYmdrYdiGlBTy9eLVhYCjKgD+LmhZ+qLwO" crossorigin="anonymous"></script>
|
||||
<script src="../country-posters.js" integrity="sha384-Hz695w/9J1maAa0H9nynW2C6KvE6ltlaaTV9rtkNLDEHnxuI53dXZtiLL7JtVF55" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
59
public/doxa-maps/country-posters/country-posters.js
generated
59
public/doxa-maps/country-posters/country-posters.js
generated
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
|
||||
<script src="../../dashboard-plus-grid.js" integrity="sha384-w8aVTRyH8dlsHIjJMHrFe6QbnZaafEb3wGjykrVlqWvj+I0S5eMwXfyBHy/SS/X0" crossorigin="anonymous"></script>
|
||||
<script src="../../dashboard-plus-grid.js" integrity="sha384-5cdQ8p58ktHysAAacoO4Fa71ukeM0ZFQO3lFnzL51DdNBmrxugr0hd7nG3lmIZ5W" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
|
||||
<script src="../dashboard-plus-grid.js" integrity="sha384-w8aVTRyH8dlsHIjJMHrFe6QbnZaafEb3wGjykrVlqWvj+I0S5eMwXfyBHy/SS/X0" crossorigin="anonymous"></script>
|
||||
<script src="../dashboard-plus-grid.js" integrity="sha384-5cdQ8p58ktHysAAacoO4Fa71ukeM0ZFQO3lFnzL51DdNBmrxugr0hd7nG3lmIZ5W" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
24
public/doxa-maps/demo-map/demo-map.js
generated
24
public/doxa-maps/demo-map/demo-map.js
generated
File diff suppressed because one or more lines are too long
2
public/doxa-maps/demo-map/demo-map/index.html
generated
2
public/doxa-maps/demo-map/demo-map/index.html
generated
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <demo-map> custom element. -->
|
||||
<script src="../demo-map.js" integrity="sha384-yLc6o2sikAUcTgqEszWaO9whzFZnBAZoO4gPXJhG3tzvfKaDpK2mhAW3qXwrycDO" crossorigin="anonymous"></script>
|
||||
<script src="../demo-map.js" integrity="sha384-30eVMRgp3BZf/wqP7A2s3/n3G62zmxdAyRX1m7dktVcAXDAva/pAdALScLNtY/2D" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <demo-map> custom element. -->
|
||||
<script src="../../demo-map.js" integrity="sha384-yLc6o2sikAUcTgqEszWaO9whzFZnBAZoO4gPXJhG3tzvfKaDpK2mhAW3qXwrycDO" crossorigin="anonymous"></script>
|
||||
<script src="../../demo-map.js" integrity="sha384-30eVMRgp3BZf/wqP7A2s3/n3G62zmxdAyRX1m7dktVcAXDAva/pAdALScLNtY/2D" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
26
public/doxa-maps/doxa-maps-spa/doxa-maps-spa.js
generated
26
public/doxa-maps/doxa-maps-spa/doxa-maps-spa.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
|
||||
<script src="../../doxa-maps-spa.js" integrity="sha384-GNqHiINJmPj5jwlIbFd3WQ2FfQy9VwJ6gZEjisE/TmS23X7RjCEz5YL81gKSVD5c" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-maps-spa.js" integrity="sha384-Ce+DKsLgwfej+rgoWeaWFuMMbR3Y4H+8io/lVL+lQfX0O3iGKwDLf4remDkHGCrP" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
|
||||
<script src="../doxa-maps-spa.js" integrity="sha384-GNqHiINJmPj5jwlIbFd3WQ2FfQy9VwJ6gZEjisE/TmS23X7RjCEz5YL81gKSVD5c" crossorigin="anonymous"></script>
|
||||
<script src="../doxa-maps-spa.js" integrity="sha384-Ce+DKsLgwfej+rgoWeaWFuMMbR3Y4H+8io/lVL+lQfX0O3iGKwDLf4remDkHGCrP" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-research-map> custom element. -->
|
||||
<script src="../doxa-research-map.js" integrity="sha384-ugm8PXu3KYQmGJoQnOUaICqt07r17kQa1bINkKsrUgt89uENqvd87rxT2NBeyC9/" crossorigin="anonymous"></script>
|
||||
<script src="../doxa-research-map.js" integrity="sha384-RNehzQHiymgsOQl1VH//TZQeAQyx1F3W7tBEjS/o02V9GoAaqFISm8gt6xFn0xse" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
45
public/doxa-maps/doxa-simple-map/doxa-simple-map.js
generated
45
public/doxa-maps/doxa-simple-map/doxa-simple-map.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-fl3Eh//QzBhOEpF3OJcfZDXOdpyb/yxiQ5aEroT23Onjya6wvBlx+F2dUB6FiBZu" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-nj5ld5CCWbfFoRiOSAdTKuZqK97b9HnROryFnZI8bDC8/ASzsLeqQdK5dI71lsq+" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-fl3Eh//QzBhOEpF3OJcfZDXOdpyb/yxiQ5aEroT23Onjya6wvBlx+F2dUB6FiBZu" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-nj5ld5CCWbfFoRiOSAdTKuZqK97b9HnROryFnZI8bDC8/ASzsLeqQdK5dI71lsq+" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../doxa-simple-map.js" integrity="sha384-fl3Eh//QzBhOEpF3OJcfZDXOdpyb/yxiQ5aEroT23Onjya6wvBlx+F2dUB6FiBZu" crossorigin="anonymous"></script>
|
||||
<script src="../doxa-simple-map.js" integrity="sha384-nj5ld5CCWbfFoRiOSAdTKuZqK97b9HnROryFnZI8bDC8/ASzsLeqQdK5dI71lsq+" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-map> custom element. -->
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-fl3Eh//QzBhOEpF3OJcfZDXOdpyb/yxiQ5aEroT23Onjya6wvBlx+F2dUB6FiBZu" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-simple-map.js" integrity="sha384-nj5ld5CCWbfFoRiOSAdTKuZqK97b9HnROryFnZI8bDC8/ASzsLeqQdK5dI71lsq+" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
2
public/doxa-maps/index.html
generated
2
public/doxa-maps/index.html
generated
File diff suppressed because one or more lines are too long
66
public/doxa-maps/manifest.json
generated
66
public/doxa-maps/manifest.json
generated
|
|
@ -9,7 +9,7 @@
|
|||
"bundle": "all-people-groups",
|
||||
"scriptUrl": "./all-people-groups/all-people-groups.js",
|
||||
"output": "doxa-maps/all-people-groups/all-people-groups.js",
|
||||
"integrity": "sha384-ajyiOLGnsr8AOhrZpwopjFXwKGNk56IbWkBMD+2oYA+DSDjdtATeoF5muZoXhcEF",
|
||||
"integrity": "sha384-xK62b4nEd/ei3xDdRNOOCl9YyDRliPUvS8dtna07xFFR1nwPqaCiAHBNuxRtuaIH",
|
||||
"elements": [
|
||||
"all-people-groups"
|
||||
],
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
"scriptUrl": "./all-people-groups/all-people-groups.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./all-people-groups/all-people-groups/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-ajyiOLGnsr8AOhrZpwopjFXwKGNk56IbWkBMD+2oYA+DSDjdtATeoF5muZoXhcEF\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./all-people-groups/all-people-groups.js\" integrity=\"sha384-xK62b4nEd/ei3xDdRNOOCl9YyDRliPUvS8dtna07xFFR1nwPqaCiAHBNuxRtuaIH\" crossorigin=\"anonymous\"></script>\n<all-people-groups profile-config='{\"profile\":\"all-people-groups\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></all-people-groups>"
|
||||
},
|
||||
"bundle": "all-people-groups"
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
"bundle": "big-picture",
|
||||
"scriptUrl": "./big-picture/big-picture.js",
|
||||
"output": "doxa-maps/big-picture/big-picture.js",
|
||||
"integrity": "sha384-a9W2VPCO3zJ+1pqWQadGso72rNPTfmZgO4aoGtZN7RDFQpVQbeTvy8JBsGi9VH43",
|
||||
"integrity": "sha384-MG/HIBfHvBjodOyOgVlzhgK5sZhEg2BI4DGPBc7yUmjGM9CDWI5JMvQh9nHRbJz4",
|
||||
"elements": [
|
||||
"bp-big-picture",
|
||||
"bp-research-header",
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
"scriptUrl": "./big-picture/big-picture.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./big-picture/big-picture/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./big-picture/big-picture.js\" integrity=\"sha384-a9W2VPCO3zJ+1pqWQadGso72rNPTfmZgO4aoGtZN7RDFQpVQbeTvy8JBsGi9VH43\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./big-picture/big-picture.js\" integrity=\"sha384-MG/HIBfHvBjodOyOgVlzhgK5sZhEg2BI4DGPBc7yUmjGM9CDWI5JMvQh9nHRbJz4\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
"bundle": "country-posters",
|
||||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"output": "doxa-maps/country-posters/country-posters.js",
|
||||
"integrity": "sha384-11tC7IzM7DM66gTcAlLSb0bwA7TTkfKYmdrYdiGlBTy9eLVhYCjKgD+LmhZ+qLwO",
|
||||
"integrity": "sha384-Hz695w/9J1maAa0H9nynW2C6KvE6ltlaaTV9rtkNLDEHnxuI53dXZtiLL7JtVF55",
|
||||
"elements": [
|
||||
"country-posters-map"
|
||||
],
|
||||
|
|
@ -170,7 +170,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/countries-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-11tC7IzM7DM66gTcAlLSb0bwA7TTkfKYmdrYdiGlBTy9eLVhYCjKgD+LmhZ+qLwO\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Hz695w/9J1maAa0H9nynW2C6KvE6ltlaaTV9rtkNLDEHnxuI53dXZtiLL7JtVF55\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"countries-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -184,7 +184,7 @@
|
|||
"scriptUrl": "./country-posters/country-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./country-posters/country-poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-11tC7IzM7DM66gTcAlLSb0bwA7TTkfKYmdrYdiGlBTy9eLVhYCjKgD+LmhZ+qLwO\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./country-posters/country-posters.js\" integrity=\"sha384-Hz695w/9J1maAa0H9nynW2C6KvE6ltlaaTV9rtkNLDEHnxuI53dXZtiLL7JtVF55\" crossorigin=\"anonymous\"></script>\n<country-posters-map profile-config='{\"profile\":\"country-poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></country-posters-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -193,7 +193,7 @@
|
|||
"bundle": "dashboard-plus-grid",
|
||||
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
||||
"output": "doxa-maps/dashboard-plus-grid/dashboard-plus-grid.js",
|
||||
"integrity": "sha384-w8aVTRyH8dlsHIjJMHrFe6QbnZaafEb3wGjykrVlqWvj+I0S5eMwXfyBHy/SS/X0",
|
||||
"integrity": "sha384-5cdQ8p58ktHysAAacoO4Fa71ukeM0ZFQO3lFnzL51DdNBmrxugr0hd7nG3lmIZ5W",
|
||||
"elements": [
|
||||
"dashboard-plus-grid"
|
||||
],
|
||||
|
|
@ -213,7 +213,7 @@
|
|||
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-w8aVTRyH8dlsHIjJMHrFe6QbnZaafEb3wGjykrVlqWvj+I0S5eMwXfyBHy/SS/X0\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-5cdQ8p58ktHysAAacoO4Fa71ukeM0ZFQO3lFnzL51DdNBmrxugr0hd7nG3lmIZ5W\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -237,7 +237,7 @@
|
|||
"bundle": "demo-map",
|
||||
"scriptUrl": "./demo-map/demo-map.js",
|
||||
"output": "doxa-maps/demo-map/demo-map.js",
|
||||
"integrity": "sha384-yLc6o2sikAUcTgqEszWaO9whzFZnBAZoO4gPXJhG3tzvfKaDpK2mhAW3qXwrycDO",
|
||||
"integrity": "sha384-30eVMRgp3BZf/wqP7A2s3/n3G62zmxdAyRX1m7dktVcAXDAva/pAdALScLNtY/2D",
|
||||
"elements": [
|
||||
"demo-map"
|
||||
],
|
||||
|
|
@ -253,7 +253,7 @@
|
|||
"scriptUrl": "./demo-map/demo-map.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./demo-map/demo-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./demo-map/demo-map.js\" integrity=\"sha384-yLc6o2sikAUcTgqEszWaO9whzFZnBAZoO4gPXJhG3tzvfKaDpK2mhAW3qXwrycDO\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./demo-map/demo-map.js\" integrity=\"sha384-30eVMRgp3BZf/wqP7A2s3/n3G62zmxdAyRX1m7dktVcAXDAva/pAdALScLNtY/2D\" crossorigin=\"anonymous\"></script>\n<demo-map profile-config='{\"profile\":\"demo-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></demo-map>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -280,7 +280,7 @@
|
|||
"bundle": "doxa-maps-spa",
|
||||
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
||||
"output": "doxa-maps/doxa-maps-spa/doxa-maps-spa.js",
|
||||
"integrity": "sha384-GNqHiINJmPj5jwlIbFd3WQ2FfQy9VwJ6gZEjisE/TmS23X7RjCEz5YL81gKSVD5c",
|
||||
"integrity": "sha384-Ce+DKsLgwfej+rgoWeaWFuMMbR3Y4H+8io/lVL+lQfX0O3iGKwDLf4remDkHGCrP",
|
||||
"elements": [
|
||||
"doxa-maps-spa"
|
||||
],
|
||||
|
|
@ -304,7 +304,7 @@
|
|||
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./doxa-maps-spa/doxa-maps-spa/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-GNqHiINJmPj5jwlIbFd3WQ2FfQy9VwJ6gZEjisE/TmS23X7RjCEz5YL81gKSVD5c\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-Ce+DKsLgwfej+rgoWeaWFuMMbR3Y4H+8io/lVL+lQfX0O3iGKwDLf4remDkHGCrP\" crossorigin=\"anonymous\"></script>\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -331,7 +331,7 @@
|
|||
"bundle": "doxa-research-map",
|
||||
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
||||
"output": "doxa-maps/doxa-research-map/doxa-research-map.js",
|
||||
"integrity": "sha384-ugm8PXu3KYQmGJoQnOUaICqt07r17kQa1bINkKsrUgt89uENqvd87rxT2NBeyC9/",
|
||||
"integrity": "sha384-RNehzQHiymgsOQl1VH//TZQeAQyx1F3W7tBEjS/o02V9GoAaqFISm8gt6xFn0xse",
|
||||
"elements": [
|
||||
"doxa-research-map"
|
||||
],
|
||||
|
|
@ -347,7 +347,7 @@
|
|||
"scriptUrl": "./doxa-research-map/doxa-research-map.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./doxa-research-map/research-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-ugm8PXu3KYQmGJoQnOUaICqt07r17kQa1bINkKsrUgt89uENqvd87rxT2NBeyC9/\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-research-map/doxa-research-map.js\" integrity=\"sha384-RNehzQHiymgsOQl1VH//TZQeAQyx1F3W7tBEjS/o02V9GoAaqFISm8gt6xFn0xse\" crossorigin=\"anonymous\"></script>\n<doxa-research-map profile-config='{\"profile\":\"research-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-research-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -356,7 +356,7 @@
|
|||
"bundle": "doxa-simple-map",
|
||||
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
|
||||
"output": "doxa-maps/doxa-simple-map/doxa-simple-map.js",
|
||||
"integrity": "sha384-fl3Eh//QzBhOEpF3OJcfZDXOdpyb/yxiQ5aEroT23Onjya6wvBlx+F2dUB6FiBZu",
|
||||
"integrity": "sha384-nj5ld5CCWbfFoRiOSAdTKuZqK97b9HnROryFnZI8bDC8/ASzsLeqQdK5dI71lsq+",
|
||||
"elements": [
|
||||
"doxa-map",
|
||||
"doxa-simple-map"
|
||||
|
|
@ -373,7 +373,7 @@
|
|||
"scriptUrl": "./doxa-simple-map/doxa-simple-map.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./doxa-simple-map/doxa-simple-map/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-fl3Eh//QzBhOEpF3OJcfZDXOdpyb/yxiQ5aEroT23Onjya6wvBlx+F2dUB6FiBZu\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-simple-map/doxa-simple-map.js\" integrity=\"sha384-nj5ld5CCWbfFoRiOSAdTKuZqK97b9HnROryFnZI8bDC8/ASzsLeqQdK5dI71lsq+\" crossorigin=\"anonymous\"></script>\n<doxa-map profile-config='{\"profile\":\"doxa-simple-map\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-map>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -623,7 +623,7 @@
|
|||
"bundle": "my-upg-100-list",
|
||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"output": "doxa-maps/my-upg-100-list/my-upg-100-list.js",
|
||||
"integrity": "sha384-HnFFKkrNrn1G3n0Ytk55taH2k/0xGwgWh2vQwJZZWN6F2TTOD2X7vnMq8eqSXpnE",
|
||||
"integrity": "sha384-we1vxOSsKO6L9jEWyO9RgD7mwBuR9HsBiCkk7vYS/zAADO3T7oXkpED2XeOd8tzO",
|
||||
"elements": [
|
||||
"my-upg-100-list-map"
|
||||
],
|
||||
|
|
@ -643,7 +643,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-HnFFKkrNrn1G3n0Ytk55taH2k/0xGwgWh2vQwJZZWN6F2TTOD2X7vnMq8eqSXpnE\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-we1vxOSsKO6L9jEWyO9RgD7mwBuR9HsBiCkk7vYS/zAADO3T7oXkpED2XeOd8tzO\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -657,7 +657,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-HnFFKkrNrn1G3n0Ytk55taH2k/0xGwgWh2vQwJZZWN6F2TTOD2X7vnMq8eqSXpnE\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-we1vxOSsKO6L9jEWyO9RgD7mwBuR9HsBiCkk7vYS/zAADO3T7oXkpED2XeOd8tzO\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -671,7 +671,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-HnFFKkrNrn1G3n0Ytk55taH2k/0xGwgWh2vQwJZZWN6F2TTOD2X7vnMq8eqSXpnE\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-we1vxOSsKO6L9jEWyO9RgD7mwBuR9HsBiCkk7vYS/zAADO3T7oXkpED2XeOd8tzO\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -680,7 +680,7 @@
|
|||
"bundle": "people-groups-grid",
|
||||
"scriptUrl": "./people-groups-grid/people-groups-grid.js",
|
||||
"output": "doxa-maps/people-groups-grid/people-groups-grid.js",
|
||||
"integrity": "sha384-M3RVF4r+s9KJ2YznjQ+g3BR4XjsA4n8uhfCk97dwyiuN1BxRdeQmmMnQILV0CwJg",
|
||||
"integrity": "sha384-SKWGRiLzac7kgXqwKmZ6zztgnwonaWQuelRXos+pToKjq9s7H1DgpKb/t+4zVsIq",
|
||||
"elements": [
|
||||
"people-groups-grid"
|
||||
],
|
||||
|
|
@ -696,7 +696,7 @@
|
|||
"scriptUrl": "./people-groups-grid/people-groups-grid.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./people-groups-grid/people-groups-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-M3RVF4r+s9KJ2YznjQ+g3BR4XjsA4n8uhfCk97dwyiuN1BxRdeQmmMnQILV0CwJg\" crossorigin=\"anonymous\"></script>\n<people-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></people-groups-grid>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-SKWGRiLzac7kgXqwKmZ6zztgnwonaWQuelRXos+pToKjq9s7H1DgpKb/t+4zVsIq\" crossorigin=\"anonymous\"></script>\n<people-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></people-groups-grid>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -741,7 +741,7 @@
|
|||
"bundle": "plugin-studio",
|
||||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||
"output": "doxa-maps/plugin-studio/plugin-studio.js",
|
||||
"integrity": "sha384-VuTJ3Db517xAN7nyV6VKdM0YJH6ToDwxyCjjAEeK69x8KompgYFCHWs29Q0BkfJp",
|
||||
"integrity": "sha384-2RlLFRU2XdjBN7w+FPguj8INgj57JFg4IFT2AmdefGnR9QQLehbiiAffV45LcsFH",
|
||||
"elements": [
|
||||
"plugin-studio"
|
||||
],
|
||||
|
|
@ -769,7 +769,7 @@
|
|||
"scriptUrl": "./plugin-studio/plugin-studio.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./plugin-studio/plugin-studio/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-VuTJ3Db517xAN7nyV6VKdM0YJH6ToDwxyCjjAEeK69x8KompgYFCHWs29Q0BkfJp\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./plugin-studio/plugin-studio.js\" integrity=\"sha384-2RlLFRU2XdjBN7w+FPguj8INgj57JFg4IFT2AmdefGnR9QQLehbiiAffV45LcsFH\" crossorigin=\"anonymous\"></script>\n<plugin-studio profile-config='{\"profile\":\"plugin-studio\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></plugin-studio>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
@ -796,7 +796,7 @@
|
|||
"bundle": "prayer-walk-planner",
|
||||
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
|
||||
"output": "doxa-maps/prayer-walk-planner/prayer-walk-planner.js",
|
||||
"integrity": "sha384-+XBRYgeeX8JLpdYcc+oGtkQf+Zgc2JkFK6/tlMBh5qghF9rNAJLk1lwVxWkU0cKx",
|
||||
"integrity": "sha384-/lrJX10JhiaXgpDAv0tf4R7ycgOaBRG+8oDasqHnD9MomT8ZGiykoh5+/vCJSJsr",
|
||||
"elements": [
|
||||
"prayer-walk-planner"
|
||||
],
|
||||
|
|
@ -812,7 +812,7 @@
|
|||
"scriptUrl": "./prayer-walk-planner/prayer-walk-planner.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./prayer-walk-planner/prayer-walk-planner/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-+XBRYgeeX8JLpdYcc+oGtkQf+Zgc2JkFK6/tlMBh5qghF9rNAJLk1lwVxWkU0cKx\" crossorigin=\"anonymous\"></script>\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./prayer-walk-planner/prayer-walk-planner.js\" integrity=\"sha384-/lrJX10JhiaXgpDAv0tf4R7ycgOaBRG+8oDasqHnD9MomT8ZGiykoh5+/vCJSJsr\" crossorigin=\"anonymous\"></script>\n<prayer-walk-planner profile-config='{\"profile\":\"prayer-walk-planner\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></prayer-walk-planner>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -821,7 +821,7 @@
|
|||
"bundle": "regions-dashboard",
|
||||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"output": "doxa-maps/regions-dashboard/regions-dashboard.js",
|
||||
"integrity": "sha384-5QpUDIMXiWozhCtGWtXmkcbZPjuqaGMDKU5RrudhFcbMMWxfTI+OhnNbbhHAlYxv",
|
||||
"integrity": "sha384-jSSCXCZ3Z6JEFDmap/W92BrPfKdpjl7pMh/t/MbkN3MxY1cYAjrq3NZsUnvfsxan",
|
||||
"elements": [
|
||||
"regions-dashboard"
|
||||
],
|
||||
|
|
@ -841,7 +841,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-5QpUDIMXiWozhCtGWtXmkcbZPjuqaGMDKU5RrudhFcbMMWxfTI+OhnNbbhHAlYxv\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-jSSCXCZ3Z6JEFDmap/W92BrPfKdpjl7pMh/t/MbkN3MxY1cYAjrq3NZsUnvfsxan\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -855,7 +855,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-5QpUDIMXiWozhCtGWtXmkcbZPjuqaGMDKU5RrudhFcbMMWxfTI+OhnNbbhHAlYxv\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-jSSCXCZ3Z6JEFDmap/W92BrPfKdpjl7pMh/t/MbkN3MxY1cYAjrq3NZsUnvfsxan\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -869,7 +869,7 @@
|
|||
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-5QpUDIMXiWozhCtGWtXmkcbZPjuqaGMDKU5RrudhFcbMMWxfTI+OhnNbbhHAlYxv\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-jSSCXCZ3Z6JEFDmap/W92BrPfKdpjl7pMh/t/MbkN3MxY1cYAjrq3NZsUnvfsxan\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -878,7 +878,7 @@
|
|||
"bundle": "research-dashboard",
|
||||
"scriptUrl": "./research-dashboard/research-dashboard.js",
|
||||
"output": "doxa-maps/research-dashboard/research-dashboard.js",
|
||||
"integrity": "sha384-sQL0nI6tuJD+PpCrg4TbNH5QwazCgMmk/oLwCH7QVbtoh4uYdoPhIPdokF1WLTgT",
|
||||
"integrity": "sha384-TSoH4Y3PWqIII0JP8c5QCi67iSGzwDzp0DovZ/4JhszwI61XJd7sjc2xwI0EvbCU",
|
||||
"elements": [
|
||||
"research-dashboard-map"
|
||||
],
|
||||
|
|
@ -894,7 +894,7 @@
|
|||
"scriptUrl": "./research-dashboard/research-dashboard.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-sQL0nI6tuJD+PpCrg4TbNH5QwazCgMmk/oLwCH7QVbtoh4uYdoPhIPdokF1WLTgT\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-TSoH4Y3PWqIII0JP8c5QCi67iSGzwDzp0DovZ/4JhszwI61XJd7sjc2xwI0EvbCU\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
|
||||
},
|
||||
"bundle": "research-dashboard"
|
||||
}
|
||||
|
|
|
|||
38
public/doxa-maps/my-upg-100-list/my-upg-100-list.js
generated
38
public/doxa-maps/my-upg-100-list/my-upg-100-list.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-HnFFKkrNrn1G3n0Ytk55taH2k/0xGwgWh2vQwJZZWN6F2TTOD2X7vnMq8eqSXpnE" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-we1vxOSsKO6L9jEWyO9RgD7mwBuR9HsBiCkk7vYS/zAADO3T7oXkpED2XeOd8tzO" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-HnFFKkrNrn1G3n0Ytk55taH2k/0xGwgWh2vQwJZZWN6F2TTOD2X7vnMq8eqSXpnE" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-we1vxOSsKO6L9jEWyO9RgD7mwBuR9HsBiCkk7vYS/zAADO3T7oXkpED2XeOd8tzO" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-HnFFKkrNrn1G3n0Ytk55taH2k/0xGwgWh2vQwJZZWN6F2TTOD2X7vnMq8eqSXpnE" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-we1vxOSsKO6L9jEWyO9RgD7mwBuR9HsBiCkk7vYS/zAADO3T7oXkpED2XeOd8tzO" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <people-groups-grid> custom element. -->
|
||||
<script src="../../people-groups-grid.js" integrity="sha384-M3RVF4r+s9KJ2YznjQ+g3BR4XjsA4n8uhfCk97dwyiuN1BxRdeQmmMnQILV0CwJg" crossorigin="anonymous"></script>
|
||||
<script src="../../people-groups-grid.js" integrity="sha384-SKWGRiLzac7kgXqwKmZ6zztgnwonaWQuelRXos+pToKjq9s7H1DgpKb/t+4zVsIq" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <people-groups-grid> custom element. -->
|
||||
<script src="../people-groups-grid.js" integrity="sha384-M3RVF4r+s9KJ2YznjQ+g3BR4XjsA4n8uhfCk97dwyiuN1BxRdeQmmMnQILV0CwJg" crossorigin="anonymous"></script>
|
||||
<script src="../people-groups-grid.js" integrity="sha384-SKWGRiLzac7kgXqwKmZ6zztgnwonaWQuelRXos+pToKjq9s7H1DgpKb/t+4zVsIq" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <people-groups-grid> custom element. -->
|
||||
<script src="../../people-groups-grid.js" integrity="sha384-M3RVF4r+s9KJ2YznjQ+g3BR4XjsA4n8uhfCk97dwyiuN1BxRdeQmmMnQILV0CwJg" crossorigin="anonymous"></script>
|
||||
<script src="../../people-groups-grid.js" integrity="sha384-SKWGRiLzac7kgXqwKmZ6zztgnwonaWQuelRXos+pToKjq9s7H1DgpKb/t+4zVsIq" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
51
public/doxa-maps/plugin-studio/plugin-studio.js
generated
51
public/doxa-maps/plugin-studio/plugin-studio.js
generated
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <plugin-studio> custom element. -->
|
||||
<script src="../plugin-studio.js" integrity="sha384-VuTJ3Db517xAN7nyV6VKdM0YJH6ToDwxyCjjAEeK69x8KompgYFCHWs29Q0BkfJp" crossorigin="anonymous"></script>
|
||||
<script src="../plugin-studio.js" integrity="sha384-2RlLFRU2XdjBN7w+FPguj8INgj57JFg4IFT2AmdefGnR9QQLehbiiAffV45LcsFH" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <plugin-studio> custom element. -->
|
||||
<script src="../../plugin-studio.js" integrity="sha384-VuTJ3Db517xAN7nyV6VKdM0YJH6ToDwxyCjjAEeK69x8KompgYFCHWs29Q0BkfJp" crossorigin="anonymous"></script>
|
||||
<script src="../../plugin-studio.js" integrity="sha384-2RlLFRU2XdjBN7w+FPguj8INgj57JFg4IFT2AmdefGnR9QQLehbiiAffV45LcsFH" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <prayer-walk-planner> custom element. -->
|
||||
<script src="../prayer-walk-planner.js" integrity="sha384-+XBRYgeeX8JLpdYcc+oGtkQf+Zgc2JkFK6/tlMBh5qghF9rNAJLk1lwVxWkU0cKx" crossorigin="anonymous"></script>
|
||||
<script src="../prayer-walk-planner.js" integrity="sha384-/lrJX10JhiaXgpDAv0tf4R7ycgOaBRG+8oDasqHnD9MomT8ZGiykoh5+/vCJSJsr" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-5QpUDIMXiWozhCtGWtXmkcbZPjuqaGMDKU5RrudhFcbMMWxfTI+OhnNbbhHAlYxv" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-jSSCXCZ3Z6JEFDmap/W92BrPfKdpjl7pMh/t/MbkN3MxY1cYAjrq3NZsUnvfsxan" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-5QpUDIMXiWozhCtGWtXmkcbZPjuqaGMDKU5RrudhFcbMMWxfTI+OhnNbbhHAlYxv" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-jSSCXCZ3Z6JEFDmap/W92BrPfKdpjl7pMh/t/MbkN3MxY1cYAjrq3NZsUnvfsxan" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
|
||||
<script src="../regions-dashboard.js" integrity="sha384-5QpUDIMXiWozhCtGWtXmkcbZPjuqaGMDKU5RrudhFcbMMWxfTI+OhnNbbhHAlYxv" crossorigin="anonymous"></script>
|
||||
<script src="../regions-dashboard.js" integrity="sha384-jSSCXCZ3Z6JEFDmap/W92BrPfKdpjl7pMh/t/MbkN3MxY1cYAjrq3NZsUnvfsxan" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <research-dashboard-map> custom element. -->
|
||||
<script src="../research-dashboard.js" integrity="sha384-sQL0nI6tuJD+PpCrg4TbNH5QwazCgMmk/oLwCH7QVbtoh4uYdoPhIPdokF1WLTgT" crossorigin="anonymous"></script>
|
||||
<script src="../research-dashboard.js" integrity="sha384-TSoH4Y3PWqIII0JP8c5QCi67iSGzwDzp0DovZ/4JhszwI61XJd7sjc2xwI0EvbCU" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue