Add addEventListener/removeEventListener with DOM-model on* semantics (#573)

* feat(events): DOM-model on* semantics with addEventListener/removeEventListener

Fixes #551 (useHostStyles broken: fonts clobber theme/CSS variables)
Fixes #225 (user onhostcontextchanged and SDK hooks mutually exclusive)

The on* setters now follow the DOM event model:
- Replace semantics (like el.onclick) with getters
- Coexist with addEventListener/removeEventListener listeners
- Both channels fire on dispatch: on* handler then listeners

Introduces ProtocolWithEvents base class between Protocol and
App/AppBridge. Each notification event gets a slot with two independent
channels (singular on* handler + addEventListener listener array).

useHostStyles hooks now use addEventListener with proper cleanup,
so they compose correctly with user on* handlers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix typedoc validation warnings

- Export AppEventMap, AppBridgeEventMap, ProtocolWithEvents so they
  appear in generated docs (they're part of public addEventListener
  signatures)
- Move AppBridgeEventMap above the AppBridge class JSDoc — it had been
  inserted between the doc block and the class, orphaning the AppBridge
  docs onto the wrong type
- Drop @param callback tags from on* getter/setter pairs — typedoc
  attaches the doc block to the getter signature which has no params
- Add MethodSchema to intentionallyNotExported (internal zod helper
  referenced by the now-documented eventSchemas field)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Olivier Chafik 2026-03-30 18:48:33 +01:00 • committed by GitHub
commit 0266171b92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -77,7 +77,7 @@ export function useHostStyleVariables(
return;
}
app.onhostcontextchanged = (params) => {
const listener = (params: McpUiHostContext) => {
if (params.theme) {
applyDocumentTheme(params.theme);
}
@ -85,6 +85,8 @@ export function useHostStyleVariables(
applyHostStyleVariables(params.styles.variables);
}
};
app.addEventListener("hostcontextchanged", listener);
return () => app.removeEventListener("hostcontextchanged", listener);
}, [app]);
}
@ -145,11 +147,13 @@ export function useHostFonts(
return;
}
app.onhostcontextchanged = (params) => {
const listener = (params: McpUiHostContext) => {
if (params.styles?.css?.fonts) {
applyHostFonts(params.styles.css.fonts);
}
};
app.addEventListener("hostcontextchanged", listener);
return () => app.removeEventListener("hostcontextchanged", listener);
}, [app]);
}