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:20-27 (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 written up as D-18 in
_docs_projet/registres/dette_technique.md — 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.
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.ts:17-19andkernel/layer-manager/item-controls-seam.ts:19both 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 the payload cannot be JSON-cloned. If it can, it belongs in GeoLeafEventMap, where it also gets a typed emitter.
See
GeoLeafEventMap — the sanitised bus.