rega RQ3(b): the packet's "Has Prayer" selection showed 50 of 997 where the legend row said 699. The legend row is right and the packet was wrong. A pin's prayer count arrives TWICE. The GeoJSON property is whatever was true when the page loaded; from then on polling writes the current count with setFeatureState, and prayer-progress.js:94 colours from ['feature-state','peoplePraying'] once polling is on. So the lit pins a reader counts are feature-state, and pin-selection.js had no 'feature-state' case at all -- it resolved to undefined, to-number defaulted it to 0, and the comparison selected only the pins whose FROZEN property happened to be above zero. 50 of them. The other 649 people groups had been prayed for and the packet left them out, which is the one thing a prayer packet must not get wrong. resolveExpr and matchesExpression now take the state alongside the properties, and selectedPinIds reads it per pin off the MAP rather than off the feature -- the source still holds the GeoJSON it loaded, setFeatureState never touches it. The source is added with promoteId:'uniqueId', so the id feature-state is keyed by is the same props[idKey] the selection already uses. Degrading is deliberate in both directions: before the first poll there is no state and property expressions still select, so an early packet is thinner and never empty; getFeatureState throws on an unknown source, so it is guarded and a bad source loses the selection rather than aborting the print. ALSO, rega RQ3(a), a candidate and not yet a confirmed cause. prewarmRegionPaint wrapped any non-feature-reading paint value in a case to make it data-driven, including a zoom interpolate -- I argued for that explicitly. useMapLayers.js:319 already held the rule and I had not read it: zoom interpolation is kept by putting the case in the interpolate OUTPUTS, and "the reverse (zoom nested inside a 'case') is what breaks the layer, so don't do that." That reverse is the shape Mapbox classifies as composite before reaching for an interpolation factor the expression cannot supply, which is the class of the interpolationFactor page error. The wrap now skips zoom expressions too. A missed pre-warm costs a frame; an illegal paint costs the layer. None of the three properties it touches is a zoom interpolate at boot, so this is not proof -- it removes the only writer that can build that shape from a runtime value. check-pin-selection asserts the live count wins over the stale property, that the stale property alone selects nothing, that a map with no feature-state still selects on properties, and that a throwing getFeatureState degrades. Negative-tested: removing the case, not reading the state, and dropping the throw-guard each turn it red; restored, green. |
||
|---|---|---|
| .. | ||
| api | ||
| colors | ||
| components | ||
| composables | ||
| constants | ||
| data | ||
| i18n | ||
| layouts | ||
| lists | ||
| maps | ||
| packet | ||
| packets | ||
| scripts | ||
| stores | ||
| styles | ||
| utils | ||
| vendor/cldr | ||
| index.js | ||
| ProfileLoader.vue | ||
| README.md | ||
library/ (@map) — the shared map template
The other half of the architecture (see ../plugin-designs/README.md):
every map in plugin-designs/<your-map>/ composes the reusable, parameterized pieces that live here.
Fix something here and every map benefits on the next build; break something here and every map
breaks — read components/WARNING.md before touching shared pieces.
| Folder | What lives there |
|---|---|
ProfileLoader.vue |
The root of every bundle: parses profile-config, loads the profile, provides the stores + config |
components/ |
Shared UI — map, legends, controls, popup (PeopleGroupDetail), tables, charts, hints (README) |
composables/ |
Shared logic — map engine (useMapInstance), provider switch, shadow-DOM styles, theming |
stores/ |
Per-instance Pinia stores (ui / map / data) — provided by ProfileLoader, injected by children |
api/ |
Data-source registry (sources.json) + fetch layer — the JAMstack runtime wiring |
colors/ |
Color sets maps opt into via profile-config.colorSet |
i18n/ |
The shared string catalogs (all maps speak the same languages) |
data/ |
Small shared datasets baked at build time (big served data lives in ../data/) |
constants/ |
Shared constants |
scripts/ |
Maintainer pipelines (posters, research) — not part of any bundle |
The rule of thumb (same as WARNING.md): custom code in your app-profile, reusable code here — and things only move from the first to the second once they've earned it.