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.
|
||
|---|---|---|
| .. | ||
| all-people-groups | ||
| big-picture | ||
| cities-internet | ||
| country-posters | ||
| dashboard-plus-grid | ||
| demo-map | ||
| doxa-maps-spa | ||
| doxa-research-map | ||
| doxa-simple-map | ||
| doxa-vision-card | ||
| golden-path-check | ||
| gospel-internet | ||
| gospel-resources | ||
| key-cities | ||
| my-upg-100-list | ||
| people-groups-grid | ||
| plugin-studio | ||
| pplr-data-maps | ||
| prayer-walk-planner | ||
| regions-dashboard | ||
| research-dashboard | ||
| research-map-plus-grid | ||
| rudder-voice | ||
| stack-demo | ||
| template-bundle | ||
| upg-neighbor | ||
| wall-of-people-groups | ||
| .gitkeep | ||
| README.md | ||
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.vueprofile is the Vue instance for that map) and that map's own non-reusable components (insrc/). Nothing here leaks to another map. - Many maps per bundle. The bundler bundles one map or many — add another flat
.vueand 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
profilefield — 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 inplugin-designs/<your-map>/src/colors/(start fromtemplate-bundle/src/colors/example-mode.js). Never edit@map/colors/to recolor your map. - API — your map reads its data from the
profile-configon the page, so the same bundle points at different data with no rebuild.DataSourceManageracceptscsv(bundled file),api(one JSON URL), orrest-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/. UseuseI18n()for strings andRTL_LOCALESfor 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
.vueper bundle (simplest). - Parameterized — one profile, behavior driven by
profile-configprops (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 entrytemplate-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