The layer-manager panel seam. Emitted by kernel/layer-manager/control.ts
once the panel structure is built, so capabilities (e.g. profile-switcher)
can insert their own controls without the kernel importing them.
Lives HERE, not in GeoLeafEventMap: the detail carries live DOM nodes, which the sanitising bus (JSON-only) would destroy.
⚠️ Fires again on every panel rebuild (destroy → recreate): subscribers must be idempotent.
The plugin ↔ toolbar seam. Emitted by kernel/ui/toolbar-dispatch.ts (mobile pill,
desktop tab strip).
⚠️ This seam is one-way, and the direction matters. Plugins and capabilities LISTEN;
they do not dispatch. The kernel-side helper dispatchToolbarAction(action, element) is
the entry point for the kernel's own emitters — it is deliberately NOT exported: absent
from kernel/ui/index.ts, from kernel-exports.ts, from the global namespace, and from
every published exports subpath. Reaching it from capabilities/** is refused by the
R.8 boundary (eslint.config.mjs), and widening the barrel to reach it would drag
components.js + pill-search.js + theme.js into the importer's bundle closure. That
trade was measured, then declined, and the decision is recorded as accepted debt in
the workshop's debt register — do not re-litigate it from this line.
⚠️ This comment called that helper "the canonical entry point" until 09/08/2026, with no qualifier. The sentence was true for the kernel and false for every other reader of this contract — who cannot reach it at all. A contract that names an entry point its audience cannot call sends that audience looking for an export that was withheld on purpose.
element is the button that was clicked — plugins read it to anchor a floating menu
(editor, measure, geocoding all do). Both fields are always set by the emitter, so
neither is optional here even though several listeners historically declared them so.
The popup/side-panel action seam. Emitted by
capabilities/feature-info/render/widget-dispatch.ts when a user clicks a button
declared as type: "action" in popup.fields[].
Moved here from GeoLeafEventMap on 14/08/2026, when the detail gained
button, setBusy() and close() — the three members GeoLeaf.Popup.registerActionHandler
used to provide before ADR-07 removed it. Through the sanitising bus, button arrived as
{} and the two functions as undefined, silently: measured on the real path before the
move, not reasoned about.
⚠️ Subscribers are unaffected — the Events facade accepts keys of both maps. What the
move buys is that dispatchGeoLeafEvent("geoleaf:popup:action", …) no longer compiles.
The host ended the boot — it is not a failure. Three causes:
beforeBoot hook threw (typically an authentication gate): reason is whatever
it threw;GeoLeaf.Core.destroy() removed the boot's map while the boot was still running (a
component unmounted right after mounting): reason is "destroyed";unmount() on the handle
GeoLeaf.mount() returned, or a later mount() taking over: reason is
"unmounted" (since 3.13.0).Emitted by app/boot-core.ts and app/mount.ts, which then hide the loading veil and draw
no failure screen: that interface belongs to the host.
Lives HERE because reason may be whatever the hook threw — usually an Error, which
the sanitising bus would flatten to {}.
Map of the GeoLeaf DOM events that carry a live
HTMLElementand are therefore dispatched as rawCustomEvents, never throughdispatchGeoLeafEvent.Why a second map rather than three more keys in GeoLeafEventMap
dispatchGeoLeafEventsanitises throughJSON.parse(JSON.stringify(detail)). Putgeoleaf:toolbar:actionin the sanitised map anddispatchGeoLeafEvent("geoleaf:toolbar:action", { action, element })becomes type-legal and runtime-wrong:elementarrives at every listener as{}. The type system would be actively inviting the bug that two files in this repo already go out of their way to warn about —kernel/ui/desktop/desktop-tabs-seam.tsandkernel/layer-manager/item-controls-seam.tsboth describe themselves as mirroring the existinggeoleaf:toolbar:actionseam for precisely this reason.Splitting the map keeps both halves honest: listening is typed for everyone (the
Eventsfacade accepts keys of both maps), and emission stays impossible to get wrong.⚠ Adding a key here is a statement that at least one field of the payload cannot survive
JSON.parse(JSON.stringify(…)). If every field can, it belongs in GeoLeafEventMap, where it also gets a typed emitter.⚠️ This sentence read "the payload cannot be JSON-cloned" until 14/08/2026, and
geoleaf:popup:actionis the first key that makes the difference matter: five of its eight fields are plain JSON, three are not (button,setBusy,close). The all-or-nothing wording would have argued for splitting one event into two — a JSON half and a DOM half — which is exactly the shape the CDC asked us not to build. The criterion is the field, not the payload.See
GeoLeafEventMap — the sanitised bus.