- public/dx-maps: the dx-maps output folder (doxa-maps e651da54f), copied unchanged.
- Home, Pray, Adopt and Research: their maps load from /dx-maps/ with the same presets. They use Mapbox with the
site's token, as before, and the keyless MapLibre when there is none. Under the Research map, one new line
links to /deep-research.
- /regions: each region card gets a "View country posters" toggle (app/components/PosterSlot.vue) that opens the
region's country posters from /dx-maps/; a poster's "Show dashboard" opens the country's own page.
- /deep-research: every research map, dashboard and list in one full-page app (no site header or footer;
"Back to homepage" in the app's menu). Hidden: in no menu, not indexed.
- /posterverse: the poster plugins in one full-page app, hidden the same way.
- Each app's own language menu switches the site's language (the language is in the address); the open tab
carries over.
- Each page asks for a fresh copy of its dx-maps script before adding it, so a returning reader never runs a
stale one.
- Every other file is as it was.
- delete the bundle loader composable and the unused slot component
- strip the site-wide embed from app.vue, leaving the layout shell
- drop the runtime config keys, custom element tag and env entries
- remove the login opt-in and the preconnect to the feedback host
- site strings, map widget locales and /terms editions for ar, de, es,
fr, hi, it, pt, ro, ru and zh
- patch the standalone map bundle in place (it has no build profile) and
rebuild the two profiled bundles from the updated source locales
- fix defects the sweep surfaced: the ru vision headline read "all
unmarried people will be married by 2033", ro used popor (nation) as
the people-group label including the homepage h1, and ar inverted
"moves from unengaged to engaged"
- correct en.ts, which claimed UUPG and WAGF stay unchanged in every
edition; es ships GENANC/FMAD, pt PNANE and de UVG/VUVG
- render the filter dropdown's empty-state through i18n, so the
no_options translation it already computed reaches the component
- translateText/translateTexts call OpenRouter chat-completions; glossary
terms are prompt-injected from config/glossaries.ts, generated from the
translation repo's TSVs by scripts/build-glossaries.ts
- model resolution: per-language override in config/languages.ts →
translation.model site setting (new admin field) → TRANSLATION_MODEL env
→ anthropic/claude-opus-5, at low reasoning effort
- blank fragments are held back from the model, which drops them and breaks
the one-translation-per-fragment count the caller maps results back on
- OpenRouter failures map to a typed error with a human-readable message;
an unrecoverable one (no credits, bad key, rejected model) stops the run
instead of repeating the same call for every remaining locale
- machine translations record source 'auto-translate'; 'deepl' stays
readable for snapshots written before the switch
* replace country pages with region dashboards
- move /countries to /regions, with 301 redirects from the old URLs in
every locale
- add a region dashboard: a map of the region's countries with pins
coloured by prayer coverage, headline figures, and prayer, adoption and
engagement progress against the DOXA goals, plus a per-country table
- roll up progress stats per country and region in the shared summary
and serve them from /api/regions
- frame region maps with antimeridian-aware bounds
- add Regions to the hamburger menu and translate the new strings in all
eight locales
* put each region on the regions index in a card with a header band
The region name links to its dashboard with the site's chevron icon, and
the people-group and country counts sit centred beside it on a brand-green
header; the country list is the padded white card body.
remove "Not Adopted" badge from research page cards
hide Adoption tab on research map and rebuild bundle
remove Adopt and Engage tiles from landing page
drop step number from lone Pray tile on landing page
add clickable country link to pray page cards
- enrich pin popup with photo, country, people group, population,
religion, description, and pray / full-profile links
- fetch the extra fields for pins; reuse existing translated labels
- render photos as a centred portrait (4:5) thumbnail, never landscape
* Update daily prayer coverage threshold
* Show segmented prayer coverage status
* update prayer progress map legend wording
relabel map legend tiers to "No One Praying", "1+ People Praying", and "100+ People Praying" across all 11 locale legend files
update LABELS constant and tier comments in prayer-progress strategy
rebuild doxa-simple-map and doxa-research-map bundles
---------
Co-authored-by: corsacca <corsacca@gmail.com>
add /countries index grouped by WAGF region and /countries/[country] pages
derive countries from prayer API (slug, count, map center/zoom) via config/countries-meta and /api/countries
prerender country routes for all locales via modules/country-routes
add CountryMap component: highlight focus country, brand-green in-country pins, muted border pins, popups
show bold green note above the map stating people-group count
lock UupgsList to a country and link card country names to country pages
add i18n keys for the new strings
add framework-agnostic vimeo tiptap node (id-only src, renders player iframe)
register vimeo in shared extension set for editor and server renderer
replace youtube prompt with generic insert-video modal (detects platform)
allowlist vimeo in validator with digits-only src check
track vimeo as a lossy node for markdown round-trip detection
* maps on pray adopt and home pages
* translation of titles
* "Fixed more info button url mapping, fixed legend gets translated fields from api using language code from parent site passed as prop, fixed legend numbers for engagement mapped to propper field"
* MERGE MAP REPOS AS PUBLIC PLUGINS
* Doxa-life is now a monorepo -> embeddables built inline -> Doxa-life /public/js serves as static CDN for serving map widgets to partner DOXA sites. UNLOCKS people of the day widget.
* Launch v1 Doxa Research Maps
* Add Go Pin Indicator 2 Research Map 2 Make Disciples
* Add Go Pin Indicator 2 Research Map 2 Make Disciples - fix map slots
* Research map: parse dialects from language field; add dialect tab + selection
- Parse "Arabic, Sudanese" (comma-inverted) and "Pakistan Sign Language" (suffix-grouped)
into base language + dialect; dedup Languages tab to one row per base language.
- Move dialect rows under base languages via expandable carat in Tab 1 (family tree)
and Tab 2 (languages). Tab 3 (Dialects) shows flat "Language, Dialect" list.
- New mapStore.selectedDialect ({ key, originalLabels }) drives dialect highlight +
exact-match pin filter. Selecting a dialect dims sibling dialects + non-matching pins;
selecting a language keeps its expanded dialect children visible (lft-row-child-active).
- _raw.primary_language is the reliable source for the comma-inverted label
(p.language gets overwritten by useMapData).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-1: Dialects/Varieties tab, parent-display title, per-map geocoder placeholder
- LegendFamilyTree: add "Dialects/Varieties" as the 3rd default tab so users
can actually reach Tab 3 from the legend bar (was only reachable via state).
- LegendFamilyTree: title row now surfaces the parent of the selected child
(e.g. "Parent: Afro-Asiatic" when Arabic is selected on the Languages tab,
"Parent: Arabic" when "Arabic, Sudanese" is selected) — solves the
"what family does Arabic belong to?" problem without leaving the tab.
- LegendFamilyTree: bump dialect indent (28→40px) and add a vertical guide
line on child rows so the hierarchical nesting is visibly clear.
- GeocoderComponent: watch the effective placeholder and patch the DOM
input directly so the per-map prop can change as the active tab switches
(Mapbox geocoder has no public placeholder updater).
- research-map: pass per-tab placeholder. On the Language Families tab the
search bar reads "Search language family, language, dialect/variety";
other tabs keep the i18n default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-2: search → tab navigation for language-family + dialect kinds
useDoxaSearch.js
- Add base-language + dialect parsing (mirrors LegendFamilyTree's SUFFIX_GROUPS).
- buildIndex now derives baseLang and dialectLabel per entry; reads the comma-
inverted label from _raw.primary_language first (p.language gets overwritten
by useMapData).
- buildAggregates: new `families` bucket (keyed by familyDerived from
langFamilyByLanguage), `languages` rebased on baseLang (so "Arabic" appears
once, not "Arabic, Sudanese" + "Arabic, Levantine"), and new `dialects`
bucket keyed by base+dialect with originalLabels accumulated.
- makeAggregateFeature emits doxa-language-family / doxa-dialect place_types
with EMOJI_FAMILY (🌳) and EMOJI_DIALECT (💬). Dialect features carry
familyDerived/baseLang/dialectLabel/originalLabels for downstream wiring.
- search() output ordering: family → language → dialect (semantic-tree top-down)
before places/religions.
GeocoderComponent.vue
- Forward doxa-language-family + doxa-dialect place_types as aggregate-result
events; thread originalLabels + familyDerived + baseLang + dialectLabel.
- Clear selectedDialect too on "All DOXA Data" click (bidirectional sync).
research-map.vue
- onGeocoderAggregateResult handles 'language-family' (selectFamily +
setActiveLegendTab('family')), 'language' (with comma-prefix + suffix-
contains pin filter so "Arabic" catches "Arabic, Sudanese" and "Sign
Language" catches "Pakistan Sign Language"), and 'dialect' (exact-match
via originalLabels + selectDialect with legend-matching key
"familyKey__baseLang__dialect" so the row indicator lights up).
- onGeocoderClear also clears selectedDialect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-3: tab info popovers (ⓘ buttons with definitions)
LegendFamilyTree.vue
- Each tab in the default tabLabels now carries an `info` field (per-tab
plain-English definition). Definitions stay generic to the tab id, so
future maps using the same component can pass their own.
- Added a small ⓘ button next to each tab label. Click toggles a popover
showing the definition; click again or click outside closes; Escape
closes. Switching tabs also closes the popover.
- Click-outside uses event.composedPath() — required for correctness across
the shadow-DOM boundary (otherwise event.target is rewritten to the host
element and the contains-check would always fail, closing on every click).
- Default copy: "A language family is a group of languages descended from
a common ancestor (e.g. Indo-European, Afro-Asiatic).", "A language is a
system of communication used by a people…", "A dialect/variety is a
regional or social form of a language…".
- Tab CSS: tab button + ⓘ now share a flex wrapper (.lft-tab-info-wrap)
with position:relative so the popover anchors to the tab cell.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map: revert iter-3 tab info popovers
Per-tab ⓘ buttons + popovers duplicated the existing toolbar HelpButton (?)
and added clutter the eventual mobile rewrite would have to undo. Removing:
- ⓘ button + popover template wrapper
- info field on tabLabels default
- openInfoTab ref + toggleInfo + onDocClick + onEsc handlers
- onMounted/onBeforeUnmount document listeners + composedPath() click-outside
- .lft-tab-info-wrap / .lft-tab-info-btn / .lft-tab-info-popover CSS
Tab definitions belong in the toolbar HelpButton's "About this map" modal,
not inline on the legend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map: 3 bug fixes from screenshot QA
1. Sign Language duplicated in Languages tab — readFamily now consults
SUFFIX_GROUPS as a final fallback. Pins where imb_language_family is null
AND lookupFamilyFromLanguageLabel misses (e.g. "Pakistan Sign Language"
not in langFamilyByLanguage.json) used to fall through to "Unknown",
creating an Unknown__Sign Language bucket alongside the existing
Sign Language__Sign Language one — same display label, two rows. Both
highlighted on select because the selected-row check is by label, not
key. Now any pin matching " sign language$" returns "Sign Language" as
the family, collapsing both buckets into one.
2. X deselect button overlapped the population badge. Moved from right:6px
(inside the inner pill) to right:-22px (into the empty right caret column,
grid column -1). Bumped contrast (dark bg + white icon, theme-inverted
in dark mode) so it reads as a proper close affordance.
3. Tab 3 (Dialects/Varieties) flat rows had no dialect styling — looked
identical to Tab 2 language rows. Added lft-row-dialect + lft-name-dialect
classes so they get the 40px indent, italic font, and vertical guide-line
that distinguish dialects from base languages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map: QA building-round-1 R2 fixes (5 items)
1. Sign-language LANGUAGES are kept distinct per QA R2 A1 — partial revert of
e26e7cd which collapsed every "X Sign Language" into one base "Sign Language"
row. SUFFIX_GROUPS renamed to FAMILY_SUFFIXES and applied ONLY in the
family-resolution path. baseLang/dialect parsing leaves the full label
intact: "Pakistan Sign Language" → family "Sign Language", base "Pakistan
Sign Language", dialect null. Each variant gets its own Languages-tab row.
Both useLanguageFamilyLegendData.js and useDoxaSearch.js updated in lockstep.
2. X deselect button now lives in its OWN grid column, not absolute-positioned
inside the inner pill. Added --lft-deselect-col (24px) and used it as the
trailing column of .lft-items grid. The X is a sibling of .lft-item (which
uses display:contents, so its inner div participates in row grid columns
2 to -2; the X lands cleanly in column -1). No overlap with badges, no
negative-margin overflow into nothing.
3. Children of selected family no longer dim (Bug 5): added lft-row-child-active
to (a) child language rows under a selected family on Tab 1, and (b) dialect
grandchildren under either a selected family OR a selected child language.
The existing CSS rule .lft-has-selection .lft-row-child-active … {opacity:1}
handles the visual exception.
4. Title-row "Field: Parent" pattern (Bug 6): replaces "Parent: X" with
level-specific labels — "Family: Afro-Asiatic" when a language is selected,
"Language: Arabic" when a dialect is selected, "Family: Sign Language"
when the family itself is selected. Default per-tab labels when nothing
is selected.
5. Cross-tab geocoder clear (Bug 11): switchTab now calls
_clearGeocoderProgrammatic() so the previous tab's search query doesn't
leak. Programmatic clear bypasses onGeocoderClear so legend selection
stays intact (only the geocoder INPUT TEXT resets).
Deferred per QA queue: Bug 7/8 count mismatches (data edge cases), Bug 9
case-insensitive search (needs repro), Bug 10 auto-collapse + scroll
(bigger UX work), file rename to SemanticTreeLegend.vue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map: QA building-round-1 R3 fixes (5 items, cherry-picked from PPLR SemanticTreeLegend)
Per the user's "merge their patterns into ours, keep our rounded edges" call —
cherry-picking from /Map-Framework/05-apps/PPLR-MAPS/.../SemanticTreeLegend.vue
rather than full port. Preserves the doxa app aesthetic.
1. Bug 13 — Parent → child selection swap now works immediately.
selectFamily() and selectLanguage() now clear the OTHER legend levels
(mutually exclusive at the store layer). Watchers in research-map.vue gate
the clear-cascade: when selectedFamily transitions to null, only fire
applyDimFilter({kind:null}) if NO other level is set — otherwise the
cascade-clear from a swap would erase the new selection via clearAllHighlights.
2. X button — now visually attached to the pill with no gap. Margin-left:-6px
eats the row's column-gap so the button left-edge sits flush with the pill's
right edge. Border-radius rounds only the right corners (matches pill's right
side). Stretches to row height; the chip "sticks out" from the pill into the
reserved trailing column. No absolute positioning, no overlap with badges.
3. Cross-tab geocoder clear — wrapped behind FEATURES.geocoderPersistAcrossTabs
(default false). Flip to true to keep the geocoder query when returning to
a previously visited tab.
4. Auto-collapse + scroll-into-view on geocoder selection (Bug 10).
research-map.vue dispatches a `legend:reveal-selected` window event after
driving mapStore.selectFamily/Language/Dialect via geocoder result.
LegendFamilyTree listens, computes the ancestor-chain keys for the selected
row, and calls setExpansionToOnly(keys) — collapsing every other family.
After nextTick, scrollIntoView(behavior:smooth, block:center) on the
.lft-row-selected element. The reveal is geocoder-only — direct legend
clicks leave the user's manual expansion state alone.
5. useLanguageFamilyLegendData exposes setExpansionToOnly(keys) — generic
helper that resets the expansion map to only the listed keys. Used by the
reveal handler above; callable by any consumer that needs to drive expansion
programmatically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map: replace LegendFamilyTree with ported PPLR SemanticTreeLegend
Per QA building-round-1 R4 user directive — "take their legend pretty much
word for word, archive ours" — the PPLR competitor's legend ships nearly
every Round-1-3 fix in one component. Cherry-pick gave way to full port:
- src/components/SemanticTreeLegend.vue (NEW): PPLR's component ported
word-for-word (.stl-* class names + script + template + CSS). Adaptations:
* Outer .stl-panel + .stl-collapse-btn + .stl-reopen rounded-pill REMOVED.
LegendDesktop's existing collapse-to-circle (the rounded aesthetic the
user explicitly called out as missing from PPLR's pill style) wraps and
drives collapse via the #title-caret slot.
* <style scoped> → useShadowStyles, since doxa-research-mfe renders in
shadow DOM (Vue's scoped attribute selectors don't bridge the boundary).
* Light-mode selectors rebased from .stl-panel[data-theme=light] to
.semantic-tree-legend[data-theme=light] to match the new root element.
- src/composables/usePplrInstance.js (NEW): verbatim copy of PPLR's per-map
instance store. createPplrInstance/provideInstance/useInstance API.
- src/components/LegendFamilyTree.vue → LegendFamilyTree.legacy.vue:
archived in place per user direction. Not imported anywhere now.
- src/composables/useLanguageFamilyLegendData.js: new langTree computed
exports the family→language→dialect tree as { id, label, color, count,
pop, filter, originalLabels?, children }. Each node carries a pre-built
Mapbox filter expression so legend selection drives the pin filter
directly with no kind-specific dispatch downstream.
- src/components/LegendDesktop.vue: import SemanticTreeLegend instead of
LegendFamilyTree. Invoke useLanguageFamilyLegendData here to derive
langTree and pass via :nodes. Translate the new component's @select
payload back into the existing legend:highlight window event so
applyDimFilter in research-map.vue can keep using the same pipeline.
Pass detail.filterExpr through verbatim — applyDimFilter prefers the
pre-built expression over rebuilding from kind+expectedValue.
- src/components/LegendMobile.vue: same swap (SemanticTreeLegend + langTree
adapter).
- src/app-profiles/research-map.vue: createPplrInstance + provideInstance
so the legend's useInstance() inject resolves. Watch uiStore.theme and
mirror to instance.theme for light/dark chrome. applyDimFilter prefers
detail.filterExpr (pre-built node filter from langTree) over kind-based
rebuild — same end result, less duplicate logic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-2: PPLR port runtime fixes (4 items)
Iter-1 (commit 2508ea3) ported the legend; build passed but four runtime
issues surfaced when traced through the actual data flow:
1. langTree family filter was unreliable. Pin features have
`properties.languageFamily` derived from `pg.languageFamily` (the API
field) which is null for all 2,069 records — so the property is mostly
"Unknown". A filter `['==', ['get', 'languageFamily'], 'Indo-European']`
would match zero pins. Fix: collect every language string under the
family (base lang + every dialect's originalLabels) and filter via
`['in', ['get', 'language'], ['literal', allLangs]]`. The `language`
property IS reliably populated. Falls back to the property match only
when the family bucket has no language strings (defensive).
2. Geocoder → legend selection was not bridged. Search picks write to
mapStore.selected{Family,Language,Dialect}, but the SemanticTreeLegend
reads from instance.selection. Added watchers in research-map.vue that
project mapStore changes onto pplrInstance.selection (using id strings
that match the langTree node ids: fam:X / lang:__X / dial:X__Y__Z).
Loop guard via the legend's existing _lastInternalId catches the
round-trip when a legend-row click → mapStore mutation → bridge fires.
3. Selection from the bridge was sparse ({id, label, depth}, no children),
so collectAllDescendantIds returned an empty Set — descendants dimmed
instead of staying full-opacity. Added findNodeById helper and made
selectedDescendants resolve the sparse selection to the full tree node
via props.nodes lookup.
4. Collapsed-state CSS targeted the old .lft-* / .lrg-* classes; the new
.stl-* SemanticTreeLegend would have shown its full inner card inside
the 36px collapsed pill, double-chrome-ing. Added equivalent rules:
.legend-container.collapsed .semantic-tree-legend .stl-tabs-wrap /
.stl-col-hdr / .stl-rows hidden; .stl-inner background/border/shadow
stripped; .stl-titlebar padding tuned to fit 36px height. Preserves
the existing rounded-circle collapse aesthetic the user emphasized.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map: SemanticTreeLegend now an EXACT clone of PPLR's; mounted standalone
Per user directive: "do a direct clone, only shave the API." Iter-1's adapted
port had stripped PPLR's outer .stl-panel + .stl-collapse-btn + .stl-reopen
chrome on the assumption that LegendDesktop's circular-collapse would wrap
better. The user disagreed — PPLR's collapsed pill + right-justified UPG
columns look better. Reverting to verbatim PPLR shape.
SemanticTreeLegend.vue
- File rewritten as a true word-for-word clone of upstream (851 lines).
- Outer .stl-panel + .stl-collapse-btn (titlebar caret) + .stl-reopen (rounded
pill collapsed-state) ALL preserved. The only deviations from upstream:
* <style scoped> contents copied into a useShadowStyles() call so the CSS
lands inside doxa-research-mfe's shadow root (Vue scoped styles don't
bridge custom-element shadow boundaries cleanly here).
* useInstance() import path points to our local copy of usePplrInstance.js
(already added in iter-1; unchanged).
research-map.vue
- langTree + LANG_TABS + onSemanticTreeSelect moved up from LegendDesktop.
The legend mounts as a direct sibling of LegendDesktop in the
rm-legend-desktop-slot. Conditional v-if swaps based on activeLegendType:
language-family → SemanticTreeLegend; everything else → LegendDesktop.
- onSemanticTreeSelect uses PPLR's setFilter(node.filter) pattern verbatim.
Mirrors selection back to mapStore so the geocoder + popup + fly-to paths
keep working unchanged. Loop guarded by the legend's _lastInternalId.
- Bridge watchers now look up the FULL node in langTree (color/children/
filter all populated) instead of passing a sparse {id,label,depth}.
Searching "Arabic" in the geocoder now writes to mapStore.selectedLanguage,
the bridge finds the matching lang: node, the legend lights up the row,
expands its ancestor chain, and PPLR's setFilter(node.filter) applies.
LegendDesktop.vue
- Reverted iter-1 changes: dropped SemanticTreeLegend import + langTree
compute + onSemanticTreeSelect translator. Left a no-op v-else-if for
legendType === 'language-family' since the parent v-if hides this whole
card on that tab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map: stop auto-switching tabs on legend row click
Per user: clicking a child component on the Family tab was auto-switching
to the Language tab. This was a bridge bug, NOT a legend-clone deviation:
1. User clicks child language "Bengali" under Indo-European on Tab 1.
2. Legend's selectNode sets _lastInternalId = "lang:Indo-European__Bengali",
writes the clicked node to instance.selection. instance watcher fires;
guard catches (sel.id === _lastInternalId), no tab switch. ✓
3. onSemanticTreeSelect → mapStore.selectLanguage("Bengali").
4. Research-map bridge watcher fires for selectedLanguage. _findNodeInTree
searches by LABEL "Bengali" — matches the FIRST lang: node it finds.
If Bengali appears in multiple family buckets (e.g. one pin under
Indo-European, one with null imb_language_family bucketed under
"Unknown"), the find may return "lang:Unknown__Bengali" — a different
id than the one selectNode used.
5. Bridge writes that wrong-id node to instance.selection. Legend watcher
fires AGAIN; guard MISSES (sel.id="lang:Unknown__Bengali" ≠
_lastInternalId="lang:Indo-European__Bengali"). Watcher reaches the
`activeTab.value = Math.min(sel.depth, ...)` line → tab switches to
depth 1 (Language tab). Bug.
Fix: in _bridgeSelection, if the legend already holds a selection of the
same kind+label, skip the overwrite. The click-set selection (which has the
correct family-prefixed id) wins; the bridge no-ops. Geocoder picks (where
no prior selection exists, or the label genuinely differs) still update.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map: restore pin-click → people-group detail on language-family tab + align geocoder column
Pin-click flow was broken on the language-family tab after the PPLR-port
mounted SemanticTreeLegend standalone (LegendDesktop hidden). Tracing the
existing flow:
- Direct pin click → useMapEvents.js → uiStore.selectPeopleGroup(feature)
→ uiStore.setLegendMode('detail'). LegendDesktop watches both and renders
PeopleGroupDetail in its content slot.
- Geocoder PG pick → mapStore.selectFeature(feature). The root-level
PeopleGroupDetail (research-map line ~1370) renders.
Two parallel detail-display paths, two stores. Pin-click on the
language-family tab fell into a gap because LegendDesktop was v-if-hidden,
so uiStore.selectedPeopleGroup changes had nowhere to render.
Fix: re-mount LegendDesktop on the language-family tab specifically when
detail-mode is active. Its existing v-if priority renders PeopleGroupDetail;
SemanticTreeLegend hides for the duration. When the user closes the detail
panel (uiStore.selectPeopleGroup(null) → legendMode goes back to 'prayer'),
the condition flips and SemanticTreeLegend reappears.
Also aligned the geocoder column (.mapboxgl-ctrl-top-left) to match the
SemanticTreeLegend's .stl-panel — left:8px, width:380px — so the search bar
and the legend stack at the same x-edge as one PPLR-style column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-2: align geocoder pill geometry + focus ring to PPLR
Audited the live geocoder against PPLR's .pgc-input-wrap; three deltas:
- border-radius was 20px → bumped to 999px (true pill, matches PPLR)
- no :focus-within ring → added rgba(59,70,61,0.18) (light) /
rgba(115,161,127,0.22) (dark, PPLR's #73A17F accent at 22% opacity)
- input font-size unset → set 13px / placeholder 12px to match
(a) v-if/v-else-if branching for LegendDesktop ↔ SemanticTreeLegend
confirmed sound — no flicker beyond Vue's normal mount/unmount.
(c) Confirmed no live LegendFamilyTree imports; all hits are stale comments
+ the archived .legacy.vue file (not imported anywhere).
(d) @media(max-width:767px) mobile geocoder override still works on top of
the desktop width:380px change (uses !important).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map: 3 bugs from desktop QA — pin-color, family-filter coverage, detail-card width
User report: clicking Afro-Asiatic in the legend only highlighted SOME
Arabic pins (the ones whose `language` happened to match a string in the
family bucket). Clicking Arabic itself highlighted MORE pins than Afro-
Asiatic. Also: Arabic pins on the map painted black instead of family-green
when the user expanded under Afro-Asiatic. Same root cause for both:
Pin features carried `languageFamily: 'Unknown'` because the API field
`imb_language_family` is null for all 2,069 records. The legend's family
bucketing derives client-side via langFamilyByLanguage; the PIN PROPERTY
never did. Result: the languageFamily color strategy painted Unknown =
fallback (gray/black), and any property-based family filter missed every
pin.
Fix#1: useMapLayers.js now derives `languageFamily` client-side via the
same lookup the legend uses (langFamilyByLanguage with comma-inversion +
parenthetical strip + sign-language suffix fallback). Pin colors now reflect
the legend's bucketing.
Fix#2: langTree's family filter is now belt-and-suspenders — matches by
derived `languageFamily` property OR by enumerated language strings under
the family bucket. Property path catches pins after the iter-2 derivation;
language-list path catches any pin whose `language` value matches a label
the legend bucketed even if the property derivation missed.
Fix#3: people-group detail card was wider than the SemanticTreeLegend
(LegendDesktop's --map-legend-width was 400px; .stl-panel is 380px). The
swap when the user clicks a pin now reads as same-card different-content.
applyContainerScale() bumped 400→380 at width≥700; .legend-container CSS
re-anchored to top:60px / left:8px / width:380px to match .stl-panel
exactly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map: ROOT-CAUSE fix — useMapData.deriveLanguageFamily now does comma-inversion
f72de45 fixed useMapLayers to derive languageFamily client-side, but the
fallback never ran. Tracing:
1. useMapData.loadData() runs FIRST during mfe boot.
2. Inside, it calls deriveLanguageFamily(pg) which did only an exact-match
lookup `langFamilyByLanguage[langLabel]`.
3. API returns pg.language = "Arabic, Sudanese", but
langFamilyByLanguage keys are standard form ("Sudanese Arabic"). Exact
lookup misses → returns ''.
4. canonicalFamilyName('') returns 'Unknown'.
5. pg.languageFamily = 'Unknown' for ~90% of the dataset.
6. useMapLayers' OR-chain `pg.languageFamily || pg._normalized?... ||
derive...` short-circuits at step 1 because 'Unknown' is truthy.
7. Pin features carry languageFamily='Unknown'; map paints them gray/black;
property filter matches nothing.
Real fix: upgrade useMapData.deriveLanguageFamily to mirror the legend's
lookupFamilyFromLanguageLabel logic — comma-inversion ("Arabic, Sudanese"
→ "Sudanese Arabic"), parenthetical strip ("Ainu (China)" → "Ainu"),
sign-language suffix fallback. Now pg.languageFamily = "Afro-Asiatic" for
every Arabic pin (and equivalents for other families).
Pin colors render correctly. Family filter (`['==', 'languageFamily',
familyKey]`) catches all pins. Belt-and-suspenders language-list filter from
f72de45 stays as a safety net for future format variances.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map mobile iter-1: SemanticTreeLegend embedded-in-sheet overrides
User reports the mobile bottom-sheet legend doesn't render on the language-
family tab. Root cause traced: LegendMobile renders <SemanticTreeLegend>
inside .legend-content for legendType==='language-family', but the
PPLR-ported component was designed for STANDALONE desktop use — its
.stl-panel uses position:absolute with hardcoded left:8px / top:60px /
bottom:12px / width:380px, anchoring to the nearest positioned ancestor.
Inside the bottom sheet, that means the panel pins to the sheet's
boundaries with desktop-sized offsets, clipping/overflowing the small
sheet chrome. .stl-reopen + .stl-collapse-btn (the desktop chrome) ALSO
duplicate the sheet's own pull-tab + .mobile-collapse-caret.
Fix: in LegendMobile's useShadowStyles, override the embedded
SemanticTreeLegend's positioning so it flows inside the sheet content area
normally (position:static, width:100%, height:100%, transparent inner card).
Hide .stl-reopen and .stl-collapse-btn since the sheet provides its own.
Trim .stl-titlebar padding so it doesn't fight the sheet's left-padding
(which clears the absolute mobile-collapse-caret at left:6px).
Iter-2 plan: verify the mobile sheet itself appears (uiStore.isMobile +
@media(max-width:767px) breakpoint alignment); audit pin-click → detail
flow on mobile; verify the sheet's height transitions don't clip the
embedded tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map mobile iter-2 + UPGs/Pop center-align (4 items)
User QA feedback:
1. uiStore.init() never called — breakpoint stayed 'desktop' even on mobile;
resize observer never attached. Wired up in research-map.vue's onMounted
(uiStore.init?.() with optional chaining for any setup that doesn't
provide an init).
2. Mobile collapse caret looked unstyled vs the desktop's "professional gray
highlighted button" (PPLR's .stl-collapse-btn). Restyled
.mobile-collapse-caret with the same gray bg + border + rounded shape;
added .sheet-dark variant for dark theme.
3. 3-tier collapse footer (collapsed → 44px footer with no rounded corners,
only the title visible). Existing CSS used height:auto which depended on
inner content; locked to height:44px / min-height:44px so the footer is
stable. Hide SemanticTreeLegend's .stl-tabs-wrap / .stl-col-hdr / .stl-rows
when the sheet is in collapsed state, and tighten .stl-titlebar to 44px
(clears the absolute mobile-collapse-caret at left:10px via padding-left:44px).
4. UPGs/Pop columns: changed from text-align:right + justify-content:flex-end
to text-align:center + justify-content:center on both .stl-hdr-num and
.stl-num so header label and numeric values share an x-center across rows.
Pin-click → mobile detail flow (LegendMobile.vue:362 PeopleGroupDetail
branch) verified working independently of LegendDesktop's parent v-if;
unchanged in this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map mobile iter-3: tab-change reset + stronger caret styling
Sub-agent traced the language-family-tab "stuck cycle" bug:
- uiStore.legendState defaults to 'collapsed' on store creation, but is
NEVER reset on tab change.
- Switching tabs keeps the prior tab's legendState. If user left Prayer at
'fullyOpen' (the user perceives this as "70%") and switches to Language
Families, the legend opens at 'fullyOpen'.
- toggleLegend cycles fullyOpen → open → collapsed → open. So clicks go
fullyOpen → open → collapsed (footer). On other tabs this works.
- On the language-family tab, the cycle SHOULD also reach 'collapsed', but
the user reports it gets stuck flipping fullyOpen ↔ open. Most likely
cause: SemanticTreeLegend's height:100% override + internal scroll
containers may have been masking the 'collapsed' state visually, but
resetting on tab change short-circuits the issue regardless.
Fix#1 (research-map.vue:355): the activeTabId watcher now calls
uiStore.collapseLegend?.() on every tab change (skipping the very first
mount since the store already starts at 'collapsed'). All tabs — including
language-family — now start fresh at the footer tier when the user
navigates to them.
Fix#2 (LegendMobile.vue:40): bumped the .mobile-collapse-caret styling
to match PPLR's light-theme .stl-collapse-btn more closely. Background
rgba(208,215,222,0.5) (visible gray fill, not a barely-there transparent
overlay), border #afb8c1 (1px stronger contrast), color #3b463d (darker
text), 1px shadow for subtle elevation. Sheet-dark variant updated to
rgba(110,118,129,0.22) bg + #30363d border for visibility on dark sheet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-4: collapsed footer layout, search ranking, geocoder hard-clear
User QA on iter-3:
1. Caret overlapped "LEGEND" title in collapsed footer (caret left:10 + 24px
width landed at x=34, title started at x=28 from .legend-content padding).
2. Title "centered horizontally but at top of footer" — wasn't vertically
centered.
3. "LEGEND" text minute (10px font on a 44px footer).
4. Searching "arabic" lowercase didn't surface the Arabic LANGUAGE entry —
five Arabic-speaking people-groups ate the All-Data slot.
5. Search bar text persisted "Afro-Asiatic" across tab switches even though
switchTab calls _clearGeocoderProgrammatic.
Fixes:
1+2+3. LegendMobile.vue collapsed-state CSS now: padding-left:44px on
.lrg-title-row / .lft-title-row / .stl-titlebar so title clears the
absolute caret with a 10px gap; display:flex + align-items:center +
height:48px on every title-bearing element so titles vertically center;
.stl-tb-title / .lrg-title font-size bumped 10→13px; sheet height 44→48px
for slightly more breathing room. Hide-rules for data rows/columns
consolidated into one selector list.
4. useDoxaSearch.js: reorder allFlat so families → languages → dialects
come BEFORE people / places / religions. With activeFilter set, the
All-Data section now takes (families×3, languages×3, dialects×3,
people×5, places×2, religions×2) instead of slicing the flat list at 5
— guarantees representation across kinds. Total cap bumped to MAX_TOTAL+8.
5. _clearGeocoderProgrammatic now force-resets the DOM input element
directly (input.value=''), in addition to calling Mapbox's geocoder.clear().
Mapbox sometimes leaves the prior aggregate-result's text in the
<input> even after clear() returns; forcing the value=''+input event
guarantees the visible text resets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-5: mobile caret z-index, padding, title alignment, mixed-case
QA round on iter-4 (commit f42062f). All visual fixes per user feedback:
1. Caret z-index too low — caret was hidden BEHIND the legend grid on the
language-family tab. Bumped from z-index:3 → z-index:10 so the caret
floats above .stl-rows / .lrg-items.
2. Caret too large — reduced from 24×22 → 20×20 (square, smaller, sits
tighter against the sheet's top-left corner). Top:14px / left:8px
alignment matches the LegendRows title-row vertical center.
3. .legend-content left padding — reduced 28px → 8px. The wide gutter was
only there to keep clear of the absolute caret; with z-index:10 the
caret floats over the grid so the gutter is unnecessary. Title rows
handle their own 36px padding-left to clear the caret visually.
4. SemanticTreeLegend title was UPPERCASE ("LANGUAGE FAMILIES") via PPLR's
.stl-tb-title { text-transform:uppercase; letter-spacing:0.07em }. The
user's other tabs use mixed-case ("Prayer Progress", "Engagement
Progress"). Override text-transform:none + 14px system-ui font for
.legend-mobile-sheet .stl-tb-title so it reads "Language Families"
matching the other tabs' aesthetic. Dark-mode color variant added.
5. Caret overlapping the "L" of "LANGUAGE FAMILIES" in expanded state —
.stl-titlebar's left padding bumped to 36px, clearing the 8+20=28px
caret + 8px breathing room.
6. Collapsed-state titles weren't horizontally centered (LegendRows
centered fine, .stl-titlebar left-justified). Added justify-content:
center to all three title-row selectors when collapsed; symmetric
padding 36px both sides keeps the centered text from touching the caret
on the left or any future close-button on the right.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-6: fix CSS specificity collision + descender clip
User QA on iter-5: caret overlapping title text on language-family tab
("guage Families" visible — "Lan" cut off behind caret) and "Legend"
descender clipped on Prayer/Engagement/Adoption tabs.
Root cause #1 (overlap): iter-1 added a defensive .stl-titlebar rule
(padding:0 12px 4px 0!important) at line 134 to "trim padding fighting
mobile sheet". Iter-5 then added padding-left:36px!important at line 95
to clear the caret. Same specificity, line 134 wins by source order →
padding-left collapses to 0, caret covers the first 28px of title text.
Removed the iter-1 padding override entirely (its purpose is now served
by line 95 + the collapsed-state padding rule); kept the border-bottom-
color tweak.
Root cause #2 (descender clip): iter-5 set line-height:1 on collapsed
title text. With font-size:13px, the line box equals the font-em-square
exactly; descenders of 'g' / 'p' / 'y' hang below the box and get
clipped by the parent's overflow:hidden. Bumped to line-height:1.4 so
the line box accommodates descenders cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-7: per-tab legend tier memory + bright-row badge contrast
User QA on iter-6:
1. Switching tabs always collapsed the legend — user wants returning to a
tab to RESTORE whatever tier (collapsed/open/fullyOpen) it was last at.
2. White text on bright green rows (engagement-tab "Engaged", adoption-tab
"Has Adoption") was unreadable. The .lrg-name label had a text-shadow
(line 170) but the .lrg-badge-inner did not, AND the badge background
was mixing the row color with currentColor=white (lightening it on
bright rows — the wrong direction for contrast). Numbers in the pills
("26", "8.3M") nearly disappeared.
Fixes:
1. research-map.vue: replaced the iter-3 "always collapseLegend on tab
change" with a Map<tabId, legendState> that saves the outgoing tab's
legendState and restores the incoming tab's prior state (default
'collapsed' on first visit). Switching Prayer→Engagement→Prayer now
returns Prayer to its prior tier instead of always-footer.
2. LegendRows.vue:
- .lrg-badge-inner background mix changed from currentColor 22%
(lightens on bright rows in light mode) to #000 28% (darkens any
row, gives white text proper contrast).
- Added text-shadow:0 1px 2px rgba(0,0,0,0.4) on .lrg-light
.lrg-badge-inner and .lrg-dark .lrg-badge-inner — same shadow
density the label gets, so "26"/"8.3M"/etc. stay legible on
bright backgrounds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-8: bump --lrg-caret-col on mobile so caret stops overlapping title
User QA on iter-7: caret still overlapped the start of title text on
Prayer/Engagement/Adoption tabs. DevTools inspector showed .lrg-items
with no left padding.
Root cause: .legend-row-group declares --lrg-caret-col:10px (line 106 of
LegendRows.vue). The .lrg-items grid uses that as the width of column 1
(default template: var(--lrg-caret-col) minmax(80px,1fr) auto auto
var(--lrg-caret-col)). Title rows place their text in column 2.
The absolute-positioned .mobile-collapse-caret is 20px wide at left:8px,
so its right edge sits at x=28. Column 1 occupies only x=0..10 + 6px
column-gap, so column 2 (where title text starts) begins at x=16 —
DEEPLY inside the caret's footprint (caret right edge x=28 vs title text
left edge x=16 = 12px overlap).
Fix: override the --lrg-caret-col custom property to 32px ON THE MOBILE
SHEET ONLY. Column 1 now spans x=0..32 + 6px gap, column 2 starts at
x=38 — well past the caret's right edge at x=28. No overlap. Column 5
(right gutter) widens symmetrically, keeping the row visually balanced.
User's suggested approach: "increase the padding on both sides of the
prayer, engagement and adoption map. And then place the caret on the
column one to the left of the title." This is exactly that — bumping
the caret column width is equivalent to adding padding around the title
pill, while the caret continues to absolute-position cleanly within the
now-wider column 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-9: slotted mobile caret + symmetric padding cleanup
Replaces the iter-5..8 absolute-positioned .mobile-collapse-caret with a
slotted caret that lives in the title row's column-1 caret slot — matching
desktop's pattern. SemanticTreeLegend now exposes a #title-caret slot
inside .stl-titlebar (with v-if fallback to PPLR's default .stl-collapse-btn);
LegendRows already had .lrg-title-caret-slot at grid-column:1.
CSS cleanup follow-on:
- .mobile-collapse-caret: dropped position:absolute / top:14 / left:8 / z-index:10;
it's now a normal in-flow flex item that aligns with the title via the slot's
align-items:center
- .legend-content padding: 12px 0 12px 8px → 12px 0 (symmetric, no asymmetric
left gutter to clear the now-removed absolute caret)
- --lrg-caret-col on mobile: 32px → 28px (still fits 20px button + 4px each side)
- .stl-titlebar expanded: dropped padding-left:36px hack; now padding:8px 12px 8px 8px
- collapsed-state title rows: padding:0 36px 0 36px → padding:0 16px with gap:8px;
caret + "Legend" cluster centered together, no fixed offset
Outcome: caret aligns with title text via grid/flex (no magic numbers); padding
is symmetric on both sides; structure-fix replaces five iterations of CSS patches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-10: fix tier-3 fullyOpen white-space-at-bottom on mobile
When the mobile sheet was at fullyOpen tier (calc(100% - 80px)) and the
legend list was long (e.g. Language Families with 30+ entries), the area
below the visible rows showed empty white space instead of more rows
flowing into view.
Root cause: .legend-content was display:block with flex:1. Its child
shells (.stl-panel / .legend-row-group) used height:100% to fill it,
but height:100% requires the parent to have a definite height — flex:1
gives a definite COMPUTED height but no definite DECLARED height, so
height:100% falls back to auto. The shells then sized to natural content,
.stl-rows / .lrg-items lost their flex:1 height anchor (their parent was
also auto), and the inner scroll containers ended up shorter than the
sheet — leaving the gap at the bottom.
Fix:
- .legend-content now display:flex; flex-direction:column; min-height:0
- Scope flex:1; min-height:0 to .stl-panel and .legend-row-group only
(PeopleGroupDetail keeps its natural-height + outer-scroll behavior)
- .stl-panel mobile override: drop height:100%, add min-height:0
Outcome: inner scroll containers now bound to the .legend-content viewport,
last row aligns to the bottom of the sheet, no white tail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-11: add min-height:0 to .stl-rows so SemanticTreeLegend scrolls
iter-10 fixed white-space-at-bottom for Prayer/Engagement/Adoption (LegendRows
path) but missed the Language Families tab (SemanticTreeLegend path) — there
was still a white gap below the visible rows on fullyOpen mobile.
Root cause: .stl-rows has flex:1 + overflow-y:auto, but no min-height:0.
Flex items default to min-height:auto = content height, so a long list grows
past the parent instead of scrolling. .lrg-items in LegendRows already has
min-height:0 in its base CSS, which is why iter-10 worked there. .stl-rows
was missing it.
Fix: scoped mobile override .legend-mobile-sheet .stl-rows{min-height:0;}.
Outcome: SemanticTreeLegend's .stl-rows now scrolls within its parent bounds
on tier-3 fullyOpen, no white tail below.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-12: clicking empty map clears stuck row-dim filter
User reported: clicking a legend row dims non-matching pins (correct), but
clicking off the row left the dim state stuck — pins stayed dimmed and
the row stayed selected with no obvious way to recover.
Fix: added a generic map.on('click') handler in onMapReady that fires
AFTER any layer-specific click. If queryRenderedFeatures finds no hits
on the interactive layers (language-family-pins, regions-fill), and any
of the prayer/engagement/adoption/family/language/dialect/region filter
states are active, clear them via uiStore setters + clearAllHighlights().
Outcome: clicking anywhere on empty map restores all pins to full opacity
and clears the row selection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Simple-map iter-1: unify engagement+adoption green to neon, move prayer-page map
User feedback (2026-05-03):
1. The simple-map (used on home/prayer/adopt pages) had different greens for
engagement (#73A17F muted sage) vs adoption (#22c55e bright lime). User
wants them unified to a single bright neon green that matches the pin
color AND the legend row color in BOTH tabs.
2. The Prayer Progress map was at the bottom of the prayer page, after the
"Choose a people group" section. User wants it moved up to sit right
after the hero/prayer-goal/women-photo section so it's more visible.
Changes:
- doxa-simple-map-mfe/src/config/colorStrategies.js: ENGAGEMENT_COLORS
hasEngagement and ADOPTION_COLORS hasAdoption both → #39ff14 (matches
research-mfe engagement neon). Single source of truth — pins + legend
rows pull the same hex.
- app/pages/pray.vue: Moved the <section> wrapping <DoxaMapSlot map-id="pray-map">
from after "Choose a people group" (line 194-201) to immediately after the
hero section (line 81), placing it before "Where do I start?".
- Built doxa-simple-map-mfe with PUBLISH_TO_PUBLIC=true so public/js/
doxa-simple-map.iife.js is updated for the parent Nuxt site to serve.
NOT pushed to remote — local commit for user to test in doxa-website-nuxt
locally before any push.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-13: slim mobile rows + free-drag legend pull-tab
User feedback (2026-05-03):
1. Mobile prayer/engagement/adoption legend rows look chunkier than the
thinner desktop equivalents — match desktop's compact density.
2. Pull-tab on language families / data legend tabs auto-snaps to fullyOpen
on the slightest upward drag. User wants free-drag on legend tabs (so
they can interact with the legend AND the map at the same time without
it taking over the screen). Keep auto-fullyOpen-snap behavior for
PeopleGroupDetail (legendMode === 'detail'), since they DO want a slight
pull to fill the screen with the photo + people-group content.
Changes:
- LegendMobile.vue handlePullTabTouchEnd: branch on legendMode. In 'data'
mode, leave the dragged height in place (keep _customLegendHeight set)
unless dragged below ~12% of available height (then collapse). In 'detail'
mode, retain the existing ladder-snap (open ↔ fullyOpen ↔ collapsed).
- LegendMobile.vue CSS: .lrg-item-inner min-height 36→28, vertical padding
6→2; .lrg-items row-gap 6→3. !important to win specificity over the
shared LegendRows.vue base rules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-14: fix legendMode check (was 'data', should be !== 'detail')
iter-13 introduced the mode-aware pull-tab release but checked legendMode
=== 'data', which never matches — uiStore.legendMode is 'prayer' by
default and only flips to 'detail' when a people group is selected. The
free-drag branch never ran, so the snap-to-fullyOpen path kept firing on
the legend tabs.
Fix: invert to legendMode !== 'detail'. Now legend tabs free-drag, and
PeopleGroupDetail still snaps to fullyOpen on a slight upward pull.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-15: snap legend to default 30% on pin select / detail close
User feedback (2026-05-03): the iter-14 free-drag fix let the user park the
legend at any custom height (e.g. 50%), but clicking a pin (entering detail
mode) inherited that custom height. The PeopleGroupDetail view is designed
around the canonical 30% 'open' state — the floating mini-image is
position:absolute with bottom:calc(30% + 16px), so a 50%-tall sheet hides
it, and the pull-tab from a custom-height detail behaves oddly because the
ladder-snap logic compares against the dragged height not the canonical
state height.
Fix: in uiStore.selectPeopleGroup, reset legendState to 'open' and clear
_customLegendHeight whenever a people group is selected (entering detail)
OR cleared (leaving detail). This restores the canonical 30% layout in
both transitions, matching the user's expected flow:
- User drags legend up to 50% in language-family/prayer/etc. tab
- Clicks a pin → legend SNAPS to 30%, floating image appears above
- X out of detail → legend stays at 30% (clean default state)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-16: collapsed-detail footer for PeopleGroupDetail mode
User feedback (2026-05-03): when in detail mode (people-group selected),
collapsing the sheet via pull-tab tap or drag-down didn't render a clean
footer — PeopleGroupDetail's rich content (image / actions / quick-stats /
story) tried to render at 48px height. The collapse worked, but the visual
result was a clipped/overflowing detail panel instead of the canonical
"caret + name" footer that the legend tabs get.
Fix:
- Added a dedicated .collapsed-detail-footer block in LegendMobile,
rendered v-if="legendState === 'collapsed' && legendMode === 'detail'".
Mirrors the LegendRows collapsed-state pattern: a centered caret + the
people-group name as the title. Click handler routes to handleCaretClick
to re-expand back to 30% open.
- PeopleGroupDetail v-if now also gates on legendState !== 'collapsed' so
it doesn't render at 48px.
- detail-close-btn (top-right X) is hidden when collapsed so the footer
reads cleanly; user re-expands via the caret to reach the X.
- New CSS for .collapsed-detail-footer + .collapsed-detail-title (light +
dark mode color).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-17: fix duplicate label in collapsed-detail footer
iter-16 added a collapsed-detail-footer (caret + PG name) but its v-if
gate on PeopleGroupDetail (legendState !== 'collapsed') let the fallback
v-else-if branches still evaluate — so SemanticTreeLegend (or LegendRows)
ALSO rendered with its own titlebar showing the same legendTitle (the PG
name). Result: the name appeared twice in the collapsed footer.
Fix: wrap the detail-mode branch in a <template v-if="legendMode ===
'detail' && selectedPeopleGroup"> so the v-else-if alternatives never
render when a people group is selected, regardless of state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-18: wire mobile SemanticTreeLegend @select to onSemanticTreeSelect
User reported: clicking a language-family legend row dimmed pins (correct),
but deselecting (clicking same row or X-chip) did NOT restore non-dim state.
Root cause: the mobile <LegendMobile> mounts its own <SemanticTreeLegend>
instance but did not pass an @select handler. Desktop's instance does:
<SemanticTreeLegend @select="onSemanticTreeSelect" .../>
On mobile, row clicks updated only the shared instance.selection (via
inject), never reaching mapStore.selectFamily → applyDimFilter →
clearAllHighlights. Whatever path was applying dim on mobile (likely the
shared pplrInstance affecting desktop's SemanticTreeLegend in the hidden
desktop slot) ran the dim once on selection but the deselect never
fired the symmetric clear because mapStore.selectedFamily was never set
to begin with → its watcher never ran the kind:null path.
Fix:
- research-map.vue: provide('onSemanticTreeSelect', node => onSemanticTreeSelect(node))
- LegendMobile.vue: inject('onSemanticTreeSelect') + wire @select on
the embedded <SemanticTreeLegend>
Outcome: mobile language-family row clicks now drive the canonical
mapStore.selectFamily → applyDimFilter pipeline, and deselect restores
pins to full opacity via the same kind:null clear path desktop uses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Simple-map iter-2: slim rows + darken badge bg for neon green readability
User feedback (2026-05-04): two issues on the simple-map after iter-1
unified engagement+adoption to neon #39ff14:
1. The bright neon row background made the badge digits hard to read
(badge bg was lightening with currentColor 22%, washing out white text)
2. Rows felt chunky vs the slimmed research-map mobile rows
Fix (mirrors research-map iter-7 + iter-13):
- .lrg-badge-inner background: color-mix with #000 28% (darken) instead of
currentColor 22% (lighten); add text-shadow:0 1px 2px rgba(0,0,0,0.45)
on the badge inner so digits stay readable on any row color.
- .lrg-item-inner: min-height 36→28, vertical padding 6→2.
- .lrg-items row-gap: 6→3.
Built with PUBLISH_TO_PUBLIC=true so public/js/doxa-simple-map.iife.js
is updated for the parent Nuxt site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Simple-map iter-3: flat top edges on collapsed mobile footer
Mirrors research-map's collapsed-footer pattern. When the simple-map
mobile legend is collapsed (52px footer mode), the rounded top corners
flatten so the footer reads as a clean strip pinned to the bottom edge.
Rounded corners return automatically when the sheet expands, since the
base .legend-mobile-sheet rule still sets border-top-*-radius:16px and
only .collapsed zeroes them out.
Single-line CSS addition — no behavior changes elsewhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-19: swap Adoption before Engagement in tab order
User feedback (2026-05-04): tab order should follow the Prayer → Adopt
→ Engage → Language Families flow, not Prayer → Engage → Adopt.
Single-line reorder in ALL_TABS array. Per-tab logic, filters, watchers,
and Mapbox layer paint all key off `id`, not array position, so no other
changes needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-20: isolate .rm-root stacking so internal z-index doesn't leak
User reported: the geocoder pill (z-index:1200 inside the shadow root)
overlapped the page hamburger menu items.
Root cause: .rm-root had position:absolute but no z-index/transform/etc.
that would force a stacking context. Without one, child stacking values
(geocoder 1200, legend 1000) compared against page-level elements rather
than only against each other inside the widget. Any page element with
z-index < 1200 (or auto) lost the stacking battle.
Fix: add isolation:isolate to .rm-root. This is the modern, side-effect-
free way to create a stacking context — no visual change, no impact on
existing internal layering. All internal z-indexes now compare only
against each other within the .rm-root subtree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* DoxaMapSlot: inherit border-radius onto the custom element host
User reported a brief square-corners flicker on the research page —
the map slot itself had rounded corners (class="rounded-xlg") but the
custom element rendered with rectangular edges for the first few hundred
milliseconds between page paint and IIFE registration / first map paint,
making the edges appear to "snap" rounded once the map loaded.
Fix: the :deep(doxa-map) / :deep(doxa-research-map) selector now sets
border-radius:inherit + overflow:hidden, so the host element matches
the slot's curve from first paint regardless of mount timing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* DoxaMapSlot: add contain:paint for reliable canvas clipping
iter-1 (efd7800) added border-radius:inherit + overflow:hidden on the
custom element host, but the user still saw a brief square-corners flash
on the research page during initial load. Some browsers don't reliably
clip a WebGL canvas (Mapbox) to a parent's border-radius until after
the first composited repaint — overflow:hidden alone is sometimes not
enough when shadow-DOM-painted canvas content is involved.
Fix: add contain:paint to the host. This explicitly creates a paint
containment layer so the border-radius clip is enforced from the very
first frame regardless of when Mapbox's canvas paints its first tile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Research map iter-21: bump desktop tab-bar left padding to clear rounded corner
User feedback (2026-05-04): on desktop the Prayer tab sat partially
under the .rounded-xlg slot corner. Mobile padding (16px) was already
correct and shouldn't change.
Fix: split the desktop padding into start:36px / end:16px. The mobile
@media(max-width:767px) override below already resets both sides to
16px so mobile is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Ship PPLR data maps page at /research/pplr-data-maps
Adds the validation MFE bundle behind a Nuxt page that reads the
Mapbox token from runtimeConfig.public.mapboxToken (NUXT_PUBLIC_MAPBOX_TOKEN),
matching the pattern used by /research and /pray. Token is never
hardcoded — sourced from .env at runtime so secret-scanning stays clean
and tokens rotate via the host environment.
Page renders the gem-frame chrome + tab system natively in Vue, then
injects /js/pplr-data-maps.iife.js on mount. The iife defines the
<doxa-map> custom element; the dynamically created doxa-map placeholders
upgrade automatically once customElements.define runs.
Source MFE:
port-kingdom/Map-Framework/05-apps/PPLR-MAPS/PPLR-validate-data-mfe
Built with `npm run build` -> dist/pplr-data-maps.iife.js, copied here.
mapbox-gl + geocoder + d3 stay as CDN scripts (intentionally NOT bundled).
Tabs through 10 PPLR validation profiles: Religion, ROP Tree, Language
Families, Semantic Trees, PG Name Locales (primary) + Sunburst, Pins,
Rosetta Cards, Coverage (More), + Index (fixed right).
Heads up: pg-names.jsonl is 54 MB (over GitHub's 50 MB advisory but
under the 100 MB hard limit). If we want to dodge the warning later,
swap to LFS or move that one file to a CDN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add /api/maps/token endpoint, swap PPLR page back to static HTML
The Vue page conversion (previous commit) was forced by GitHub's
secret-scanning false-positive on the hardcoded Mapbox pk. token. This
commit ports the dt-geo-steward pattern (geo-steward.php :: get_temp_key)
to a Nuxt server endpoint instead, which lets us go back to a clean
static HTML page in public/.
Changes:
+ server/api/maps/token.get.ts — returns a pk.* directly OR mints a
1-hour read-only tk.* from an sk.* via Mapbox Tokens API. Caches
minted TKs in-memory until 5 min before expiry. JWT-decodes the
SK to extract the username for the API call (Mapbox 'u' field).
M nuxt.config.ts — adds private mapboxKey runtimeConfig slot
(NUXT_MAPBOX_KEY). Falls back to public.mapboxToken if unset, so
existing setups keep working with no env change.
+ public/research/pplr-data-maps.html — static page that fetches
/api/maps/token at load, sets window.PPLR_TOKEN, then injects
/js/pplr-data-maps.iife.js. Token NEVER appears in committed
files — secret-scanning safe.
D app/pages/research/pplr-data-maps.vue — replaced by the static
HTML above. Same URL semantics (/research/pplr-data-maps.html now,
no Nuxt routing involved).
Pattern can be reused by any future static MFE embed on doxa-life:
just fetch /api/maps/token, set the token, load your bundle. No
runtimeConfig dance, no page wrapper.
Heads up: requires NUXT_MAPBOX_KEY (sk. or pk.) OR the existing
NUXT_PUBLIC_MAPBOX_TOKEN to be set in Railway env. If only the public
token is set, the endpoint returns it as-is — same effective result
as before, just routed through the server.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Maps round 2: prayer green unify, geocoder flag, black pin, default tab
User feedback (2026-05-04) — four changes for the simple-map and the
research-map's language-families legend:
1. Simple-map prayer green: was '#27ae60' (dark), now '#39ff14' (neon)
so all three "green" rows (prayer fullPrayer, engagement hasEngagement,
adoption hasAdoption) match exactly across all three simple-map tabs.
2. Simple-map geocoder: feature-flagged off via FEATURES.geocoder=false
in doxa-simple-map.vue. Research-map keeps its geocoder unchanged.
Flip the flag back on if/when search returns to home/pray/adopt pages.
3. Simple-map selected-pin marker: orange GO pin → simple black pin.
Removed the orange glow halo, the orange shimmer animation, the
"GO" text label, and the dark inner circle. Just a black teardrop
with white outline + a tiny drop shadow for visibility.
4. Research-map Language Families legend: default tab is now "Language"
(index 1) instead of "Language Family" (index 0). Set on the shared
pplrInstance after creation so both desktop and mobile mounts inherit
it. Users can still click the Language Family tab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Maps round 3: unify greens to #22c55e, simple black pin on research, legend reflow when geocoder hidden
User feedback (2026-05-06) — three Phase-1 fixes for shipping V1:
1. Unify green across BOTH maps to '#22c55e' (the existing adoption green —
"deep but visible, not too dark, not too neon"). Replaces:
- simple-map prayerColors.fullPrayer '#39ff14' → '#22c55e'
- simple-map ENGAGEMENT_COLORS '#39ff14' → '#22c55e'
- simple-map ADOPTION_COLORS '#39ff14' → '#22c55e'
- research-mfe ENGAGEMENT_COLORS '#39ff14' → '#22c55e'
- research-mfe prayerColors.fullPrayer '#27ae60' → '#22c55e'
- research-mfe ADOPTION_COLORS '#22c55e' (already correct, no change)
Now all 6 "green" rows + pin colors match exactly across both bundles.
2. Apply the simple black/white selected-pin pattern to research-mfe (mirrors
what simple-map iter-1 received earlier). GO_ORANGE constant '#FF6600' →
'#1a1a1a' (black). createGoMarkerElement body simplified — no orange
glow halo, no shimmer animation, no inner dark circle, no "GO" text.
Just a black teardrop with a white outline + a tiny drop-shadow.
3. Simple-map: when FEATURES.geocoder=false, the LegendDesktop reflows up
from top:54px to top:10px to fill the freed slot. Driven by adding a
'dsm-no-geocoder' class to .dsm-root (bound to !FEATURES.geocoder) +
a single CSS rule that overrides the legend-container top.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Maps round 4: research caret pattern on simple-map mobile + slight padding trim
User feedback (2026-05-06) — Phase 2 of the V1 ship:
Simple-map mobile legend: now mirrors the research-mfe collapsed-footer
pattern so the caret reads more clearly and the title is centered:
- .mobile-collapse-caret bumped from a 10x10 monochrome arrow to the
research-mfe 20x20 light-gray pill (border + box-shadow + hover state).
- Inner SVG bumped 8x8 → 11x11, stroke-width 2 → 1.5.
- --lrg-caret-col bumped on mobile only (10 → 28px) so the slotted
20px pill fits with breathing room and doesn't overlap the title.
- Collapsed-state .lrg-title-row reflows to display:flex; justify-content:
center; gap:8px — caret + "Legend" cluster now sits in the middle of
the 52px footer instead of being subgrid-pinned to column 1.
Research-mfe mobile legend: slight padding trim per the "find the middle
ground" ask:
- --lrg-caret-col 28px → 24px. Pill caret is 20px so still fits with 2px
breathing room on each side; row content gets ~8px more width.
Note: not using module federation, so the caret pattern is duplicated
between the two LegendMobile.vue files. Acceptable for v1 ship.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Maps round 5: unify needs-red, rename Languages tab, dynamic selection-pin color
User feedback (2026-05-06) — three V1 ship items:
1. Needs-red across all "not" rows: notEngaged + notAdopted in BOTH bundles
change from near-black '#1a1a2e' to red '#e74c3c' so prayer's noPrayer,
engagement's notEngaged, adoption's notAdopted all share the SAME red.
Same approach as the green unification: prayer-color drives the family.
2. Tab + legend rename "Language Families" → "Languages" on research-mfe
(research-map.vue ALL_TABS label + desktop SemanticTreeLegend title +
LegendMobile fallback title). Tab id and ALL underlying logic still keys
off 'language-families' / 'language-family' so no other code touched.
3. Selected-pin color now matches the underlying dot color via a new
provide('getActivePinColor', ...) in each app-profile. useSelectedPin.js
in BOTH bundles now:
- injects the resolver
- tracks closure-state _currentColor (default GO_ORANGE fallback)
- rebuilds the marker SVG when color changes
- updates the highlight layer's circle-color paint property
Resolver in research-map.vue uses getColorStrategy(activeTab.colorStrategy)
directly; simple-map's mapping is via COLOR_MODES (engagement/adoption/
prayer) since its tab strategyKey strings don't match COLOR_MODES values.
Note: pin updates on next selection — if user switches tabs while a
group is selected, pin stays the previous color until next click. Can
add a tab-switch watcher in a follow-up if it's a real annoyance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Maps round 6: angled brush-style pin + 'advanced research map' link on simple-map pages
User feedback (2026-05-06):
1. Selected pin redesign — replaced the flat-fill teardrop with an angled
gradient-filled version that reads more like a hand-drawn / painterly
pin (per the reference image they shared). In both useSelectedPin.js
files:
- viewBox bumped to "-2 -2 32 40" so a -5° tilt around the tip (14, 35)
doesn't clip the pin's top corners.
- <linearGradient> in <defs> with three stops: lighter ${color}@92%,
${color}@100%, then #000@85% bottom-right — gives the pin volume.
- filter: drop-shadow(2px 4px 3px rgba(0,0,0,0.4)) on the SVG —
stronger offset shadow that suggests the pin is set down on the map.
- Wrapper bumped 28x36 → 32x40.
- Each marker render generates a unique gradient ID via
pinGrad-${Math.random().toString(36).slice(2,8)} so multiple pins on
the same page can never collide on the gradient ID.
- The dynamic ${color} from round 5 still drives the gradient stops,
so the pin color follows the active dot color AND has the painterly
gradient at the same time.
2. "Explore the advanced research map →" link added below the simple-map
on home (index.vue), pray (pray.vue), and adopt (adopt/index.vue) pages.
Uses NuxtLink + localePath('/research') to match the rest of the site's
routing. Right-aligned, underlined, brand color. Gives simple-map users
a clear path to the more advanced 5-tab research-map widget.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Maps round 7: rephrase + enlarge 'Explore our research maps' link
User feedback (2026-05-06):
- Text: 'Explore the advanced research map' → 'Explore our research maps'.
Drops 'the advanced' (more inclusive phrasing) and pluralizes 'maps'
since the research page hosts five tabs / five distinct map experiences.
- Font size: 0.95rem → 1.2rem so the link reads as a deliberate CTA below
the simple-map and not as fine-print.
Applied to home (index.vue), pray (pray.vue), adopt (adopt/index.vue).
No MFE rebuild needed — pure Nuxt page text change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Maps round 8: untilt selected pin + theme-aware thin Joshua-Project-style data-pin outline
User feedback (2026-05-06):
1. Selected pin was visibly tilted (-5° from round 6) and looked off; user
wants a fully symmetric vertical pin. Removed the <g transform="rotate(
-5 14 35)"> wrapper around the pin paths in BOTH useSelectedPin.js
files. Painterly gradient + drop-shadow + dynamic color all preserved
— just no tilt.
2. Data-pin outline reverse-engineered from Joshua Project — they use a
very thin near-black outline that reads as a subtle definition rather
than the bright white halo we had. In light mode (default boot style
= mapbox/light-v11) the white #ffffff outline made red/orange/green
pins look "haloed" and washed out. New approach:
- Initial circle-stroke-color in addLanguageFamilyLayer:
'rgba(0,0,0,0.45)' — thin, semi-transparent near-black for light mode.
- Watcher on isDark in each app-profile flips stroke to '#ffffff' when
the user toggles to dark mode (so pins stay visible against dark
basemaps), and back to 'rgba(0,0,0,0.45)' on light.
- The existing setStyle()-then-re-add path in handleToggleTheme already
re-applies the layer; the new watcher just ensures stroke updates
reactively in both directions.
stroke-WIDTH (getCircleStrokeWidthInterpolation('standard')) already
ramps 0.5px → 1.5px from min to zoom 12, which is fine — no width
change needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Maps round 9: Joshua-Project outline now also flows through ACTIVE_LAYER overlay
User feedback (2026-05-06) — round 8 made the base data-pins thin near-black
in light mode, but selecting a legend row still drew the matched pins
through a SEPARATE 'language-family-pins-active' overlay layer whose stroke
color was hardcoded white via getStrokeColor() returning '#ffffff' (old
FEATURES.darkPinBorder=false gate). Result: filtered/highlighted pins kept
the old bright white halo while non-matched dimmed pins had the new thin
outline — visually inconsistent.
Fix in simple-map app-profile:
- getStrokeColor() drops the FEATURES.darkPinBorder gate; now returns
isDark.value ? '#ffffff' : 'rgba(0,0,0,0.45)' to match round 8's
base-layer watcher.
- getFilterStrokeColor() simplified — the DARK_PIN_KEYS exception
(returned white for 'notEngaged'/'notAdopted') no longer matches its
original intent because round 5 changed those pins from near-black to
red (#e74c3c). All filter cases now use getStrokeColor() uniformly.
Research-mfe was already fine — it dims via circle-(stroke-)opacity on the
same language-family-pins layer rather than drawing a separate active
overlay, so the round-8 stroke-color change covers it without extra work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Maps round 10: feature-flag the active-overlay emphasis off on simple-map
User feedback (2026-05-06): selecting a legend row added too much visual
noise — the matched pins got a thicker outline, an enlarged radius, AND
a pulsing animation via a separate ACTIVE_LAYER + PULSE_LAYER overlay.
Want just "dim the others" and call it done.
Added FEATURES.activeOverlayEmphasis (default false). When false:
- initActiveLayer() short-circuits at the top — neither ACTIVE_LAYER nor
PULSE_LAYER ever get added, so no rAF loop, no animation, no extra stroke.
- applyLegendFilter dim path: instead of recoloring base to mutedGray and
drawing matches via the (now-absent) overlay, the base layer's
circle-opacity + circle-stroke-opacity become 'case' expressions on
matchExpr — matched pins stay at 0.9 / 1.0, non-matched dim to 0.15 /
0.15. Color stays at the tab's normal palette.
- clear-filter branch restores circle-stroke-opacity:1 (it was previously
set to 0 via the muted-gray dim path).
- Legacy overlay code is kept behind the flag in case someone wants to
flip it back on.
Visual outcome: matched pins look IDENTICAL to their unfiltered state
(thin near-black outline from round 8/9, no halo); only non-matched pins
fade. No animation. Matches what the user asked for.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Replace doxa-{simple,research}-mfe with unified 1040-maps forge
Two cloned MFEs (doxa-simple-map-mfe, doxa-research-mfe) collapsed into
a single Vite monorepo at embeddables/micro-frontends/1040-maps/, ending
the clone-and-drift pattern that was shipping divergent SemanticTreeLegend,
colorStrategies, useMapData, etc. across maps.
Architecture:
src/ shared library (components, composables, config,
stores, i18n, utils, ProfileLoader)
app-profiles/<n>/ per-bundle source folders
├─ index.html (staging)
├─ index.js (custom-element entry)
└─ profiles/ (bundle-private .vue profiles)
npm run build loops app-profiles/* → emits one IIFE per bundle
directly to ../../public/js/<bundle>.js
Per-bundle build (BUNDLE=<name> vite build) lets each bundle ship as a
single self-contained IIFE with CSS inlined via vite-plugin-css-injected-
by-js. Tree-shaking + workspace-style sharing replaces Module Federation;
nothing ships across bundle boundaries that the bundle's profiles don't
import.
Bundles produced:
public/js/doxa-simple-map.js 377 KB (3 simple-map profiles)
public/js/doxa-research-map.js 1.25 MB (1 research workbench profile,
7 outer tabs incl. new Religion)
New research-map outer tabs (vs original): Affinity Blocks (4-tier:
Bloc → Cluster → PG → PGIC), Religions (10 family rows, Christianity
filtered out via excludeReligionFamilies opt). Vertical separator
between tier-1 (Prayer/Adoption/Engagement = simple-map regurgs) and
tier-2 (Languages/Affinity Blocks/Religions = research-only).
Custom-element ownership: <doxa-map> exclusive to simple-map bundle;
<doxa-research-map> exclusive to research bundle. Disjoint to prevent
the SPA-navigation tag-collision bug where /research → /home would
have <doxa-map> stuck on the research bundle's profileModules.
Site wiring: useDoxaMap.ts paths updated to .js (no .iife suffix).
DoxaMapSlot already routes to distinct tags per bundle.
Old MFE folders (doxa-simple-map-mfe/, doxa-research-mfe/) removed —
historical archive lives at Map-Framework/1040-maps/ if needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: rename legend title from Affinity Blocks to People Groups
* feat(1040-maps): modern map styling — Apple Maps / Felt quality polish
Why: The map's GIS-tool-era styling (black borders, high-saturation fills,
dark pin outlines) undermined the polished UX the research tool needs.
Modern map products (Apple Maps, Felt.com, Mapbox Standard) use low-opacity
tinted fills, sub-pixel borders, white pin strokes, and drop shadows.
Changes:
- Polygon fill: opacity 0.35 → 0.20, add fill-antialias + 300ms transition
- Borders: #000000 width 1 → rgba(60,60,80,0.35) width 0.6
- Pin stroke: rgba(0,0,0,0.45) → rgba(255,255,255,0.9) white for contrast
- Pin shadow: new language-family-pins-shadow layer (circle-blur + translate)
- Selection dimming: selected 0.30 / unselected 0.06 (stronger spotlight)
- Unselected border: rgba(60,60,80,0.15) width 0.3 during selection
- Region insert z-order: prefer shadow layer as beforeId anchor
- All reset paths (clearAllHighlights, onSemanticTreeSelect, applyDimFilter)
updated to new baseline values
Verified: All 7 region hex colors preserved. Hitbox layer untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(legend-mobile): add doxa-regions (WAGF Regions) to mobile SemanticTreeLegend
Why: LegendMobile only gated SemanticTreeLegend for language-family and
affinity-block tabs — the doxa-regions tab fell through to LegendRows
(flat list), which can't render the 3-tier Region/Block/Country tree.
This also meant the WAGF Regions tab on mobile showed the wrong component
with mismatched data, appearing as empty/no content.
Fix:
- Import useWagfRegionsLegendData and wire regionsTreeForMobile
- Add REGIONS_TABS_MOBILE constant (Region/Block/Country)
- Update treeForMobile, tabsForMobile, treeTitleForMobile computeds
to handle 'doxa-regions' as a third branch
- Add 'doxa-regions' to the template v-else-if condition so the
SemanticTreeLegend renders instead of LegendRows on mobile
The desktop path (research-map.vue) already handles all three legend
types via legendNodes/legendInnerTabs/legendTitle computeds and the
onSemanticTreeSelect handler already processes region:/block:/country:
node id prefixes — no changes needed there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(research-map): add mobile edge scroll padding instead of cooperativeGestures
Why: on mobile, cooperativeGestures forced a two-finger-zoom requirement that
trapped users — they couldn't scroll past the map without using two fingers.
The ctrl-to-zoom overlay was also undesirable on desktop.
Fix: remove cooperativeGestures entirely (desktop gets direct scroll-zoom with
no ctrl requirement). Add two invisible 40px-wide touch strips along the left
and right edges of the map that have touch-action:pan-y, so mobile users can
grab either edge to scroll the page past the map without getting stuck.
The strips are pointer-events:none on desktop (no interference) and
pointer-events:auto only on mobile via @media(max-width:768px).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(legend): match flat legend carets + collapsed pill to SemanticTreeLegend
Why: Prayer/Adoption/Engagement/Religion tab legends used a bare 10x10px
downward-chevron caret and a plain border-radius:20px collapsed state.
SemanticTreeLegend (Languages/PG/Regions tabs) has a polished 22x20px
button caret with hover glow and a beautiful floating pill when collapsed.
The visual inconsistency made the flat legends look unfinished.
Changes:
- LegendDesktop: replace .legend-collapse-caret with STL's .stl-collapse-btn
styling (22x20px, rounded-rect, dark bg + border, green hover accent).
SVG swapped from downward chevron to STL's right-chevron with 180deg
rotation. Collapsed state now uses STL's .stl-reopen pill appearance
(border-radius:999px, dark bg, floating shadow, hover lift).
- LegendRows: widen --lrg-caret-col 10px -> 30px to accommodate the
larger caret button with breathing room on both sides.
- LegendMobile: unify .mobile-collapse-caret to 22x20px STL dimensions,
match bg/border/color/hover to STL's light and dark palettes, SVG
updated to 9x9 with 1.8 stroke-width.
- Full light/dark mode support on all three components.
Verified: npm run build succeeds (both doxa-research-map + doxa-simple-map).
No backticks in useShadowStyles strings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(legend): collapsed pill parity, pin dimming, tab scrollbar fix, legend state preservation
- LegendDesktop collapsed state now uses separate reopen pill button
matching SemanticTreeLegend's stl-reopen exactly (dark/light modes)
- Unselected pins fully disappear (opacity 0) when legend row selected,
including shadow layer
- Pin strokes: thin dark rings on light mode, white on regions/dark mode
- Legend state preserved when switching tabs (no forced collapse)
- Tab bar horizontal scrollbar hidden on desktop
- Religion "Other" color changed to vivid orange for visibility
- Collapse-deselect: collapsing parent deselects selected children
- Tab renamed "WAGF Regions" → "Regions" (legend title kept as WAGF Regions)
- Legend state preserved when clicking pin then closing detail panel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(1040-maps): prayer ripple animation, unified legend labels, mobile legend parity
Prayer ripple: CSS-keyframe-inspired expanding rings on prayed-for pins.
4 rings staggered at 1.5s intervals over a 6s cycle, scaling from 0.3x
(inside pin) to 2.5x with linear opacity fade. Color-coded by prayer
status (orange = has prayer, green = fully prayed). Glow coupled to
legend filters via syncGlowFilter. Unprayed pins render on top (z-index 3)
so they draw attention.
Legend: title always "Legend", column header shows tab-specific label
(Prayer Status, World Religions, etc.) in larger monospace text. Both
simple and research maps unified. Simple map now uses SemanticTreeLegend
on desktop + LegendMobile on mobile (was STL-only). Desktop defaults open,
mobile defaults collapsed. Tab switch preserves legend state.
DoxaMapSlot: removed bottom scroll pad that blocked mobile legend touches.
Added left/right scroll edges inside shadow DOM for simple map parity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(DoxaMapSlot): only render top scroll pad for simple maps
The top scroll pad (z-index 1500, pointer-events:auto, 48px) was blocking
touch/click events on the research map's tab bar on mobile. Research maps
have their own tab bar occupying the top area and don't need the scroll pad.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(legend): cap panel height to viewport instead of stretching to bottom on tablet
Why: On tablet (desktop layout), the SemanticTreeLegend .stl-panel used
`bottom: 12px` absolute positioning, stretching the panel from top:60px
to the viewport bottom regardless of content height. This created visible
white space below the last row on region and people-group tabs where
content is shorter than the available space. The prayer/adoption/engagement
tabs already avoided this via the `.fit-content` class which set
`bottom: auto; max-height: calc(100vh - 80px)`, but tree tabs (regions,
people-groups, language families) did not have `hideTabs` set, so the
class was never applied.
Fix: Remove `bottom: 12px` from the base `.stl-panel` rule and add
`max-height: calc(100vh - 80px)` to all panels. The flexbox column
layout naturally shrinks to content height, and the max-height cap
prevents overflow past the viewport. The `.stl-rows` container already
has `overflow-y: auto`, so scrolling activates automatically when
content exceeds the capped height. Also updated the @media(max-width:640px)
override to use max-height instead of bottom.
Verified: both bundles build cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(legend): hide SemanticTreeLegend when people-group detail opens
Why: When a pin was clicked on tablet, the SemanticTreeLegend stayed
visible behind the PeopleGroupDetail panel. The v-show directive was
applied directly to the SemanticTreeLegend component, which has two
root elements (.stl-panel div and .stl-reopen button). Vue 3's v-show
on a multi-root component fails silently — it cannot set display:none
on a fragment, so the component remained visible.
Fix: Wrap SemanticTreeLegend in a single-root <div> and move v-show
to the wrapper. The wrapper is a plain block element with no positioning
of its own — .stl-panel inside handles its own absolute positioning.
When detail mode activates, the wrapper's display:none properly hides
both the panel and the reopen pill.
Verified: both bundles build cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pins): increase touch target to 44px+ on tablet zoom levels 4-8
Why: Pin tapping on tablet required too much precision. The invisible
hitbox layer used the 'large' circle radius profile which only reached
6px radius (12px diameter) at zoom 4 and 14px (28px) at zoom 8 — well
below the 44px minimum recommended by Apple HIG and 48px by Material
Design for comfortable touch targets.
Fix: Add a 'touchTarget' interpolation profile to zoom.js with 22px+
radius at zoom 4-8 (44-52px effective diameter). The hitbox layer in
useMapLayers.js now uses this profile instead of 'large'. The visual
pins remain at their standard size — only the transparent hit-test
area grows. At zoom 0-2 the hitbox is 14-18px radius (still larger
than visual pins), scaling up to 40px radius at zoom 14.
Touch target diameters at key zoom levels:
zoom 4: 44px (meets Apple HIG)
zoom 6: 48px (meets Material)
zoom 8: 52px (comfortable)
Verified: both bundles build cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(region-popup): wire WAGF Region, WAGF Block, and UUPG count into country-click detail
Why: useMapLayers created its own internal useMapEvents instance without
passing getNormalizedPeopleGroups, so the region popup always received an
empty array — UUPG count was 0 and WAGF fields showed dashes. Additionally,
DataSourceManager only created *Label fields for {value,label} objects,
not plain strings, so APIs returning string region names produced no label
field. The doxa-api source also lacked a countryIso field mapping, causing
the ISO-3 match to fail for that data source.
Fix:
- useMapLayers now accepts and forwards getNormalizedPeopleGroups to its
internal useMapEvents, so the region popup gets the full people-group array
- DataSourceManager objectFields loop now creates *Label fields for plain
strings too (if no label already exists), making the popup resilient to
both {value,label} objects and raw string values from any API
- doxa-api sources.json gains countryIso → imb_isoalpha3 mapping parity
with pray-tools (country_code) and doxa-csv (ISOalpha3)
- generateRegionPopupContent falls back through countryIso → country for
ISO matching, and wagfRegionLabel → doxaRegionLabel → doxaRegion → wagfRegion
for display
Verified: both doxa-research-map and doxa-simple-map bundles build cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(legend): constrain desktop panel height to map container, not viewport
Why: commit db805e6 capped .stl-panel with calc(100vh - 80px), but 100vh
references the browser viewport. When the map is embedded in a page with
a header/nav above it, the legend extends past the map container bottom.
Fix: switch to calc(100% - 72px) so the max-height is relative to the
nearest positioned ancestor (.rm-map-area, position:relative). The 72px
accounts for the top:60px offset plus a 12px bottom gap. Short-content
tabs still shrink-to-fit (max-height, not height). .stl-rows already has
overflow-y:auto so a scrollbar appears when content overflows.
Mobile breakpoint (@media max-width:640px) left untouched — it correctly
uses calc(100vh - 72px) since the map fills the viewport on mobile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(research-map): add standalone embed page for partner websites
Why: partners need a simple URL they can iframe or link to directly to
show the research map on their own sites. The existing pplr-data-maps.html
is a complex multi-tab showcase unsuitable for embedding.
What: minimal full-viewport HTML page at /research/doxa-research-map.html
that fetches the Mapbox token from /api/maps/token, loads the
doxa-research-map MFE bundle, and renders a single <doxa-research-map>
custom element filling 100vw x 100vh on a dark background. No tabs, no
gem frames, no showcase UI — just the map.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(research-map): add missing dataSource and instanceId to embed profile-config
Why: the standalone embed page only passed profile + tk to <doxa-research-map>,
but DataSourceManager requires dataSource: 'pray-tools' to resolve the API
endpoint. Without it the fetch never fires and the map loads empty. Also adds
instanceId: 'embed-research' for unique map container IDs matching the pattern
used by the Nuxt research page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(research-map): add Share button with iframe embed snippet for partners
Why: partners need a frictionless way to embed the research map on their
own websites. Currently there is no share/embed affordance in the toolbar
so partners must manually construct an iframe tag or request the URL.
What:
- New ShareButton.vue following the MapControlButton wrapper pattern
- Popover with pre-filled <iframe> snippet and direct-link sections
- One-click copy-to-clipboard with "Copied!" visual feedback (2s revert)
- Embed URL computed from window.location.origin (works on staging + prod)
- Click-outside and Escape close the popover
- Dark/light theme support via useShadowStyles, mobile-responsive popover
- Added to research-map toolbar only (not simple-map)
- i18n "shareEmbed" key added to all 11 locales
- Both bundles verified: npm run build passes cleanly
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(research): add share-multiplier animated SVG for partner communication
Why: project managers need a visual that immediately conveys the value
proposition of the share/embed button — one research map deployed on
doxa.life can multiply into 5+ prayer surfaces across partner websites
via iframe embedding.
Animation sequence (10s loop):
1. Center browser shows the doxa.life research map with 10/40 window dots
2. Share icon pulses + <embed/> snippet emerges
3. Green flow lines connect to 4 partner browser windows that appear sequentially
4. All 5 maps pulse with green ripple — "1 → 5x prayer surfaces" label fades in
Style: dark theme (#0b0f14), green prayer accent (#22c55e), blue share accent
(#60a5fa). Pure CSS animations, no JS, standalone SVG, under 20KB.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(research): replace share-multiplier SVG with two-panel light-mode design
Why: the dark-mode (#0b0f14) SVG clashed with the light website context.
The new design communicates the share-to-embed story more clearly with
a two-panel layout: a partner-facing hero (65%) showing the doxa.life
browser -> share button -> 3 partner sites animation, and a developer
pipeline strip (35%) showing the Vue -> Vite -> bundle -> iframe flow.
Visual: white background, cream/beige landmass map, blue share button
(#2563eb), green people-group dots (#22c55e), animated blue flow lines,
sequential partner window activation with green glow, 10s CSS loop.
24.8KB, pure SVG + CSS keyframes, no JavaScript.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(research): two-panel dark-theme share-multiplier SVG
Why: the share-multiplier visual needs to serve two audiences in one
graphic -- project managers who need the "one click = 5x prayer" story,
and developers who need the build pipeline schematic.
Top panel (~70% height): animated hero "MULTIPLIED"
- Center doxa.life browser with 10/40 window map + green people-group dots
- Blue share icon pulses, <embed/> snippet emerges
- 5 partner browser windows (partner-a.org, church-b.com, missions-c.net,
pray-d.org, network-e.org) appear sequentially via green flow lines
- Green ripple bursts expand from all 6 maps simultaneously
- "FOR DOXA PARTNERS" section label
Divider: gradient horizontal line + "HOW IT WORKS" label
Bottom panel (~30% height): static architecture pipeline
- 7-stage left-to-right: Vue Source -> Vite Build -> .js Bundle ->
HTML Embed Page -> /public Nuxt Host -> iframe Snippet -> Partners
- Color-coded: green=Vue, blue=Vite+iframe, amber=bundle, green=partners
- Monospace annotations map each stage to actual filenames
- "FOR DEVELOPERS" section label
Style: dark theme (#0b0f14), green prayer (#22c55e), blue share (#60a5fa).
Pure CSS @keyframes, no JS, 10s seamless loop, 23KB standalone SVG.
Replaces light-mode version from c491f99 with spec-matching dark theme.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(share): redesign ShareButton for non-technical users
Why: the original share popover led with a raw <iframe> tag and a
"Direct Link" section — fine for developers but intimidating for
ministry partners and church websites. Market research of YouTube,
Spotify, Canva, and Google Maps showed that consumer-grade share
modals always lead with "Copy Link" and hide embed behind a
friendly tab.
Changes:
- Two-tab design: "Share Link" (default) and "Add to Your Website"
- Share Link tab: clean URL display, large Copy Link CTA, email +
WhatsApp social icons
- Embed tab: plain-English description (never mentions "iframe"),
Small/Medium/Large size-preset pills that live-update the code,
styled code box with Copy Code button
- Both copy buttons show checkmark + "Copied!" feedback for 2s
- Box-with-arrow-up share icon (iOS/Material standard)
- Header with close X, smooth pop-in animation
- Full dark/light theme support via isDark prop
- Mobile-responsive: wider popover positioned below button
- All styles via useShadowStyles (shadow DOM compatible)
- Build verified: both research-map and simple-map bundles compile
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(share): portal z-index fix, touch targets, and topojson deps
ShareButton popover now portals to shadow-root level so it paints above
the mobile legend (z-1000). Touch targets bumped to 44-48px min-height.
Added topojson-client and world-atlas deps. Rebuilt research-map bundle.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(share+legend): mobile share popover positioning and collapsed legend caret
ShareButton mobile: pin popover at top:120px (clears the search bar pill),
left:10/right:52 to align with MapToolbar and leave room for the toolbar
buttons. Slim header to match SemanticTreeLegend's .stl-titlebar styling
(32px min-height, mono uppercase title). Removed Share via email/WhatsApp
row. Click-outside now only closes when click hits the bare map canvas so
interacting with the legend or other UI keeps the popover open.
LegendMobile: add max-height:none override on .stl-panel so the recent
calc(100% - 72px) cap (commit 81816a0) doesn't zero-out the panel inside
the 48px collapsed sheet, which was hiding the caret + title.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: swift-river542 <swift-river542@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
load feedback-web-component.iife.js from support.gospelambition.org
rename tag to <feedback-web-component> and update isCustomElement
set showByDefault: false so widget only shows with ?feedback=true
delete local public/assets/feedback-widget/ files
* add global feedback-widget bubble to parent site
Adds a persistent feedback bubble (bottom-right, all pages) that funnels
site-wide feedback to project id 3c0e8534-f593-4222-8315-31dda4514760.
- public/assets/feedback-widget/*: widget bundle + slot CSS (cherry-picked
from the maps branch so this branch can run standalone)
- nuxt.config.ts: register <feedback-widget> as a custom element in both
vue.compilerOptions and vite.vue.template.compilerOptions
- app/composables/useFeedbackWidget.ts: injects slot CSS and loads the
widget iife on mount (dedup guard to avoid double-loads if maps land)
- app/app.vue: mounts one <feedback-widget> outside <NuxtLayout> so it
appears on public, admin, and auth layouts
Heads-up for the future maps merge: pages with a map-anchored widget
(home/adopt/pray on the maps branch) will render two widgets — the global
bubble plus the map-specific one. Reconcile during that merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* make it more nuxt
---------
Co-authored-by: swift-river542 <swift-river542@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: corsacca <corsacca@gmail.com>