doxa-maps/plugin-designs
Doxa Maps 9fddcb9901 min-height clamps height, so the pane could not hold the plugin it was pinning
The Driver's least favourite bug: on DX Life's research page a capability-4 plugin
with tabs at the top had a second, element-level scrollbar, and scrolling it scrolled
the TAB HEADER out of view.

doxa-maps-spa pins each fixed-viewport plugin with position:absolute;inset:0;height:
100% and its own comment states the invariant: "fills the pane exactly, so
scrollHeight===clientHeight and overflow:auto paints NO scrollbar." CSS never granted
that. A used height is CLAMPED between min-height and max-height, so a child's own
min-height BEATS the height:100% the shell sets -- it is a floor, not a preference.
Wherever the pane was shorter than that floor the pinned element grew taller than the
pane it was meant to fill, .dms-slot grew with it, and .dms-pane's overflow-y:auto
became a real scroll container wrapping the WHOLE plugin, tab strip included. That is
the second scrollbar: not the plugin's content scroller, the PANE scrolling the plugin
bodily. The invariant was written as a description and was only ever a hope.

The shell now pins with min-height:0. An inline style outranks a child's :host rule,
so that one property restores the invariant for every fixed-viewport plugin at once
without editing any of them. It is NOT added to the scroll-stack branch, where
natural height is the design.

A floor set INSIDE a child's shadow root is out of reach from the shell, and two of
mine had one. research-dashboard's .rd-shell and big-picture's .bp-root now say
min-height:inherit, deferring to :host -- which the gallery pins to 0, and which a
naive page leaves at its own floor, so the collapse guard those floors exist for is
kept rather than deleted. big-picture's 480px MOVED UP to :host rather than being
dropped: :host had no floor there, so inheriting would have quietly resolved to 0
everywhere and thrown away the black-screen guard its own comment calls load-bearing.

check-one-scroll-container asserts both halves, because either alone rots: the shell
still pins with min-height:0, and no plugin grows a NEW floor inside its shadow root.
The second half is what matters later -- a fixed shell plus a plugin that adds
min-height:700px next month is this bug again and nobody would connect it.

The rule is a CONTRADICTION IN ONE DECLARATION, not a large min-height by itself: an
element saying height:100% ("I fill whatever holds me") while refusing to be shorter
than N. My first version flagged any big min-height and caught gospel-resources'
.gd-map, a map deliberately sized height:80svh that scrolls inside its root's own
content scroller -- the one scrollbar a plugin is allowed. Flagging it would have sent
its owner after a non-bug, which is worse than not having the gate.

Negative-tested: removing min-height:0 from the shell, and reverting either .rd-shell
or .bp-root to a constant, each add a failure; restored, only the known one remains.

RED ON PURPOSE, and deliberately NOT in gates:pre. gospel-internet's .gb still carries
min-height:640px and that fix is its owner's. Wiring a red gate into gates:pre would
block the fleet on someone else's file, so the gates:pre entry belongs in the same
landing that fixes .gb -- until then this gate is the work item, afterwards the guard.

Source only: doxa-maps-spa, research-dashboard and big-picture need rebuilding, which
is the build owner's to do.
2026-09-28 15:01:19 +00:00
..
all-people-groups A bare-host embed under ?lang=ar now renders Arabic here too 2026-09-27 02:17:35 +00:00
big-picture min-height clamps height, so the pane could not hold the plugin it was pinning 2026-09-28 15:01:19 +00:00
cities-internet Cities with Internet: the AI-notice dismiss button meets the 24px target minimum 2026-09-28 12:41:10 +00:00
country-posters Country posters: clearing a highlight keeps the region paints data-driven 2026-09-28 10:59:19 +00:00
dashboard-plus-grid Plugin Studio and the gallery split production from experimental, from each bundle's own stage 2026-09-27 06:04:08 +00:00
demo-map C2: one library helper for per-engine projection, across 12 call sites in 9 plugins 2026-09-26 08:39:53 +00:00
doxa-maps-spa min-height clamps height, so the pane could not hold the plugin it was pinning 2026-09-28 15:01:19 +00:00
doxa-research-map The packet was praying over the numbers the page loaded with, not the live ones 2026-09-28 13:56:22 +00:00
doxa-simple-map Prayer Progress: note the accessibility work due before the help modal or a second tab is switched on 2026-09-28 12:41:10 +00:00
doxa-vision-card Doxa Vision Card: take the page language from profile-config lang as well as locale 2026-09-26 01:10:51 +00:00
golden-path-check golden-path-check: walk the develop-a-plugin-and-see-it-live path end to end 2026-09-22 03:55:18 +00:00
gospel-internet Put the collapse guard on the host, where the rest of the fleet keeps it 2026-09-28 14:49:10 +00:00
gospel-resources Use the keys that landed for three strings the map still said in English 2026-09-28 12:57:56 +00:00
key-cities Key cities: the map honours provider=maplibre instead of always building Mapbox GL 2026-09-27 04:38:17 +00:00
my-upg-100-list UPG-100: file three India zones into the asia region 2026-09-28 15:01:19 +00:00
people-groups-grid The composed grid's locale depends on a spelling neither file mentions 2026-09-28 10:26:43 +00:00
plugin-studio library: one embedScript for the app shells, replacing four private copies 2026-09-28 07:07:33 +00:00
pplr-data-maps Pplr Data Maps: tab and legend labels follow a language change instead of keeping the one they started with 2026-09-28 13:01:16 +00:00
prayer-walk-planner Prayer Walk Planner: the phone toolbar fits, with the list actions behind More 2026-09-28 00:36:26 +00:00
regions-dashboard Localise the region link's screen-reader label 2026-09-28 12:51:28 +00:00
research-dashboard min-height clamps height, so the pane could not hold the plugin it was pinning 2026-09-28 15:01:19 +00:00
research-map-plus-grid A browser gate refuses to launch outside the browser slot; check:browser-gates-guarded keeps it that way 2026-09-28 08:20:21 +00:00
rudder-voice Rudder's robot voice keeps a sentence whole across a breath, and splits only real run-ons 2026-09-28 14:45:56 +00:00
stack-demo Decouple prose: no go-base / nuxtbase / nuxtinator / doxa-base names in plugin comments and docs 2026-09-26 17:28:45 +00:00
template-bundle template-bundle: the starter's first screen is now a real map 2026-09-26 04:38:12 +00:00
upg-neighbor UPG Neighbor: an unplaced group reads ring -1, so it can never pass for a reached one 2026-09-28 14:21:34 +00:00
wall-of-people-groups Wall of People Groups: no debug logging in the shipped bundle 2026-09-28 02:53:46 +00:00
.gitkeep Rename app-profiles -> plugin-designs; build green under new naming (load-bearing paths updated, guards pass) 2026-09-17 18:18:18 +00:00
README.md plugin-designs/README: the tree is dx-maps, and a bundle's tag is its folder name 2026-09-26 01:19:53 +00:00

plugin-designs/ — where YOUR maps live

The shared template lives in ../library/ (aliased @map) — you never edit it per-map. You build your map by making a folder here, plugin-designs/<your-map>/, and composing the template's building blocks. Your folder is yours (colors, API, i18n); the template is shared (Apache-2.0, propagated to every map on the next build).

Shared vs. yours — the balance

  library/  (@map)  ── SHARED, reusable, parameterized ──►  legend · map · map-controls
        │                                                    (every map reuses these)
        │  imported by
        ▼
  plugin-designs/<your-map>/  ── YOURS ──►  the map's "main" class (the .vue profile = the Vue
        │                                  instance for that map)
        └── src/            ── YOURS ──►  this map's OWN non-reusable components, colors, API, utils

  One bundle can hold as many maps as you want — add another .vue profile and rebuild.
  • Reusable → library/. The legend, the map, the map-controls are shared, parameterized components every map reuses. Fix once, every map benefits. Never edit it to customize one map.
  • This map's own → plugin-designs/<your-map>/. It holds two things: the map's "main" class (each .vue profile is the Vue instance for that map) and that map's own non-reusable components (in src/). Nothing here leaks to another map.
  • Many maps per bundle. The bundler bundles one map or many — add another flat .vue and rebuild.

Production or experimental — every profile declares itself

Every app profile is one of two things, and experimental profiles are unlimited and encouraged — create as many as you need:

  • Experimental, not built: prefix the folder with _ — the build skips it; it just sits in code. Zero cost, zero risk.
  • Experimental, built: builds normally, but it belongs in the Experimental section of the power app's layout (nav section) — never embedded on partner surfaces.
  • Production: a real product — main sections of the power app + individually embeddable.

(The power app = the SPA layout that composes all profiles into one application.)

The pattern in one picture

plugin-designs/<your-map>/     ← one FOLDER = one BUNDLE, named after the folder
   ├── index.js               ← entry: registers the custom element, finds the profiles
   ├── overview.vue           ← one .vue = one PROFILE (flat in the folder — no profiles/ subfolder)
   └── detail.vue             ← another profile in the SAME bundle

     builds to ─────────────► public/dx-maps/<your-map>/<your-map>.js   (one file)
  • Folder name = bundle name. Folders starting with _ or . are skipped (handy for WIP).
  • Each .vue = one profile (one map screen). One bundle can hold many.
  • Pick the profile at embed time with the profile field — every profile shares one bundle:
<script src="/dx-maps/<your-map>/<your-map>.js"></script>
<your-map profile-config='{"profile":"overview","tk":"pk.eyJ..."}'></your-map>
<your-map profile-config='{"profile":"detail","tk":"pk.eyJ..."}'></your-map>

Add a map — three steps

# 1. create the folder + one profile
mkdir -p plugin-designs/<your-map>
#    index.js   ← copy doxa-simple-map/index.js, rename the tag
#    <name>.vue ← your screen, built from @map/* blocks (flat in the folder)

# 2. build
npm run build            # → public/dx-maps/<your-map>/<your-map>.js
#    (just this one:  BUNDLE=<your-map> npm run build:bundle)

# 3. embed the script tag + element (dev staging at `npm run dev` shows a paste-ready snippet)

Your three things (everything else is shared)

  • Colors — how pins are colored. Pick one the template ships (getColorStrategy('language-family')), or drop your own in plugin-designs/<your-map>/src/colors/ (start from template-bundle/src/colors/example-mode.js). Never edit @map/colors/ to recolor your map.
  • API — your map reads its data from the profile-config on the page, so the same bundle points at different data with no rebuild. DataSourceManager accepts csv (bundled file), api (one JSON URL), or rest-api (a live REST endpoint, the default). Prefer the real API over inlined JSON.
  • i18n — the template ships 11 base locales in @map/i18n/locales/. Use useI18n() for strings and RTL_LOCALES for right-to-left; add or override strings from your own profile.

The one hosting variable

The only thing that changes between hosting modes is the <script src>: a relative path when your own site serves the file, or an absolute CDN URL when a partner site serves it. The bundle is identical either way. See ../documentation/CONTRIBUTING.md for the two CDN modes and partner-site embedding.

Profile patterns (mix freely)

  • Individual — one .vue per bundle (simplest).
  • Parameterized — one profile, behavior driven by profile-config props (e.g. tabs[]).
  • Nested — one profile that renders others (workbench-style, like doxa-research-map).

See also

  • doxa-simple-map/index.js — fully commented reference bundle entry
  • template-bundle/ — minimal two-profile skeleton to copy
  • ../library/colors/README.md — the color-strategy contract
  • ../README.md — repo overview & build pipeline · ../documentation/CONTRIBUTING.md — build vs dev, CDN modes