UI Events Helper¶
The ctx.ui.events object provides reactive subscriptions to Lumiverse UI state (keyboard, drawer, settings) and a generic DOM event delegation helper. This is particularly useful for tracking mobile keyboard dimensions and for binding interactions on extension-injected or mounted DOM elements without leaking global event listeners.
Keyboard State¶
Access the virtual keyboard presence and safe-area inset (especially on iOS PWA or Android Chrome where the window resizing or visual viewport varies).
export interface SpindleUIKeyboardState {
/** True when the host believes a virtual keyboard is currently visible. */
visible: boolean;
/** Safe bottom inset in CSS pixels that keeps content above the keyboard. */
insetBottom: number;
/** Current visual viewport width in CSS pixels. */
viewportWidth: number;
/** Current visual viewport height in CSS pixels. */
viewportHeight: number;
}
export function setup(ctx: SpindleFrontendContext) {
// Read synchronously
const state = ctx.ui.events.getKeyboardState()
// Subscribe to changes (returns an unsubscribe function)
const unsub = ctx.ui.events.onKeyboardChange((newState) => {
if (newState.visible) {
console.log(`Keyboard opened. Safe inset bottom: ${newState.insetBottom}px`)
}
})
}
Drawer State¶
Track when the side portrait drawer is opened, closed, or switched to a different tab.
export interface SpindleUIDrawerState {
open: boolean;
tabId: string | null;
}
export function setup(ctx: SpindleFrontendContext) {
const unsub = ctx.ui.events.onDrawerChange(({ open, tabId }) => {
if (open && tabId === 'my-extension-tab') {
console.log('User opened my drawer tab!')
}
})
}
Settings State¶
Track when the settings modal is opened, closed, or switched to a different view.
export interface SpindleUISettingsState {
open: boolean;
view: string;
}
export function setup(ctx: SpindleFrontendContext) {
const unsub = ctx.ui.events.onSettingsChange(({ open, view }) => {
if (open && view === 'my-extension-settings') {
console.log('User opened my extension settings panel!')
}
})
}
DOM Action Delegation¶
bindActionHandlers is a helper for binding generic interaction events (click, pointer down, etc.) onto extension-owned DOM, such as elements injected via ctx.dom.inject(), mounted via ctx.ui.mount(), or returned by ctx.ui.showModal().
Instead of adding dozens of individual .addEventListener() calls, you can define a dictionary of action handlers. The helper binds a single delegating event listener to the root container and maps events back to your callbacks.
Usage¶
export function setup(ctx: SpindleFrontendContext) {
// 1. Create your extension-owned DOM
const root = ctx.ui.mount('sidebar')
root.innerHTML = `
<div id="btn-approve" class="btn">Approve</div>
<div id="btn-reject" class="btn">Reject</div>
`
// 2. Bind action handlers
// The dictionary keys map to the "id" attribute of descendants by default.
const unbind = ctx.ui.events.bindActionHandlers(root, {
'btn-approve': (detail) => {
console.log('Approved!', detail.element, detail.originalEvent)
},
'btn-reject': () => {
console.log('Rejected!')
}
})
}
Advanced Binding¶
You can override the matching attribute and listen to different pointer events.
const root = ctx.dom.inject('body', `
<button data-action="swipe-left">Left</button>
<button data-action="swipe-right">Right</button>
`)
const unbind = ctx.ui.events.bindActionHandlers(root, {
'swipe-left': (detail) => { /* ... */ },
'swipe-right': (detail) => { /* ... */ }
}, {
attribute: 'data-action',
events: ['pointerdown', 'pointerup']
})
Note: The binding target may be the owned placement root or any owned descendant inside a registered placement root (for example, a mounted action container). The helper throws if you target system UI or another extension's DOM.
The target must be connected and live when binding. detail.root (the event.root value) is the exact target supplied to bindActionHandlers, while liveness, permission, and revocation follow its nearest registered owner root. Removing or revoking that owner root automatically unbinds the listener; the returned unbind function remains safe to call after automatic cleanup.